move blueprints out of the sidebar into Ctrl+C / Ctrl+V dialogs
The blueprint panel is gone; the side panel column now holds the selected building panel alone at full height. Saving is Ctrl+C on the current selection, and picking one for placement is a new frameless modal card grid opened with Ctrl+V, or handed to directly after a confirmed save. BlueprintPanel sheds its widget half and becomes BlueprintLibrary: the list, its disk round-trip, capture, and the T hotkey, with no widget of its own. MainWindow owns it and drives both dialogs, because ModalPauseScope needs a GameWorldView and only MainWindow has one. Both handlers hold a single pause scope and a single dim scope across the save-to-select handoff, so the dim does not blink and the simulation is not resumed in between. The two facts a card derives -- the per-type contents summary and the plain cost total -- go into lib/sim next to captureBlueprintFromSelection so they can be unit tested; src/ui is off the test include path. They are kept apart from GameWorldView's placement total, which excludes locked types and rotate-in-place targets and is a different number by design. A card's delete icon is a sibling of the card body rather than one of its children: Qt disables a widget's children along with it, and the delete has to stay live on an unaffordable card. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
11
src/lib/eventsystem/event/BlueprintSaveRequestedEvent.h
Normal file
11
src/lib/eventsystem/event/BlueprintSaveRequestedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Ctrl+C: save the current building selection as a named blueprint
|
||||||
|
// (REQ-UI-BLUEPRINT-CREATE, REQ-UI-HOTKEYS). MainWindow owns the modal flow, because
|
||||||
|
// it is the only widget that can pause the game and raise the dim overlay; whether
|
||||||
|
// anything placeable is selected is decided there, not by the key handler.
|
||||||
|
class BlueprintSaveRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
11
src/lib/eventsystem/event/BlueprintSelectionRequestedEvent.h
Normal file
11
src/lib/eventsystem/event/BlueprintSelectionRequestedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Ctrl+V: open the blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG,
|
||||||
|
// REQ-UI-HOTKEYS). The Ctrl+C path does not go through this event -- it opens the
|
||||||
|
// dialog directly so one pause scope and one dim scope span both dialogs
|
||||||
|
// (REQ-UI-MODAL-DIM).
|
||||||
|
class BlueprintSelectionRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
@@ -15,6 +15,8 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/UnlockedBuildingsChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlockedBuildingsChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSaveRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PanDirectionChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/PanDirectionChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PauseToggleRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/PauseToggleRequestedEvent.h
|
||||||
|
|||||||
@@ -3,10 +3,14 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
|
#include "BuildingsConfig.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -49,6 +53,21 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
|
|||||||
resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells;
|
resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells;
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Position of a building type in buildings.toml, which is the order the build button
|
||||||
|
// bar lays out its buttons (REQ-UI-BUILD-BAR) and so the tie-break order for a
|
||||||
|
// blueprint's contents line. A type with no config entry sorts after every known one.
|
||||||
|
std::size_t configOrderIndex(BuildingType type, const BuildingsConfig& buildings)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < buildings.buildings.size(); ++i)
|
||||||
|
{
|
||||||
|
if (buildings.buildings[i].type == type)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildings.buildings.size();
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
||||||
@@ -166,3 +185,56 @@ bool selectionHasPlaceableBuilding(const Simulation& sim,
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
|
||||||
|
const Blueprint& blueprint, const BuildingsConfig& buildings)
|
||||||
|
{
|
||||||
|
std::map<BuildingType, int> counts;
|
||||||
|
for (const BlueprintBuilding& building : blueprint.buildings)
|
||||||
|
{
|
||||||
|
++counts[building.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The config index is carried through the sort so the comparator stays a strict
|
||||||
|
// total order; the enum value is the final tie-break, which only two config-less
|
||||||
|
// types could ever reach.
|
||||||
|
struct RankedType
|
||||||
|
{
|
||||||
|
BuildingType type;
|
||||||
|
int count;
|
||||||
|
std::size_t order;
|
||||||
|
};
|
||||||
|
std::vector<RankedType> ranked;
|
||||||
|
ranked.reserve(counts.size());
|
||||||
|
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||||
|
{
|
||||||
|
ranked.push_back({entry.first, entry.second,
|
||||||
|
configOrderIndex(entry.first, buildings)});
|
||||||
|
}
|
||||||
|
std::sort(ranked.begin(), ranked.end(),
|
||||||
|
[](const RankedType& left, const RankedType& right)
|
||||||
|
{
|
||||||
|
if (left.count != right.count) { return left.count > right.count; }
|
||||||
|
if (left.order != right.order) { return left.order < right.order; }
|
||||||
|
return static_cast<int>(left.type) < static_cast<int>(right.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<BlueprintContentEntry> summary;
|
||||||
|
summary.reserve(ranked.size());
|
||||||
|
for (const RankedType& entry : ranked)
|
||||||
|
{
|
||||||
|
summary.push_back({toDisplayName(buildingTypeId(entry.type)), entry.count});
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings)
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
for (const BlueprintBuilding& building : blueprint.buildings)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = buildings.findBuildingDef(building.type);
|
||||||
|
if (def) { total += def->cost; }
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
class Simulation;
|
class Simulation;
|
||||||
|
struct BuildingsConfig;
|
||||||
|
|
||||||
// The user-configurable settings of a single building or construction site: the
|
// The user-configurable settings of a single building or construction site: the
|
||||||
// selected recipe / ship schematic, the shipyard module layout, and (for
|
// selected recipe / ship schematic, the shipyard module layout, and (for
|
||||||
@@ -49,6 +50,29 @@ Blueprint captureBlueprintFromSelection(const Simulation& sim,
|
|||||||
const std::vector<BuildingId>& selectedIds);
|
const std::vector<BuildingId>& selectedIds);
|
||||||
|
|
||||||
// True if any selected id refers to a player-placeable building or construction site
|
// True if any selected id refers to a player-placeable building or construction site
|
||||||
// (the enable condition for the Create Blueprint button, REQ-UI-BLUEPRINT-CREATE).
|
// (the condition under which Ctrl+C opens the blueprint save dialog,
|
||||||
|
// REQ-UI-BLUEPRINT-CREATE).
|
||||||
bool selectionHasPlaceableBuilding(const Simulation& sim,
|
bool selectionHasPlaceableBuilding(const Simulation& sim,
|
||||||
const std::vector<BuildingId>& selectedIds);
|
const std::vector<BuildingId>& selectedIds);
|
||||||
|
|
||||||
|
// One "<building name> x <count>" entry of a blueprint card's contents line
|
||||||
|
// (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
struct BlueprintContentEntry
|
||||||
|
{
|
||||||
|
std::string buildingName;
|
||||||
|
int count;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Summarizes what a blueprint holds: one entry per building type it contains, ordered
|
||||||
|
// by descending count with ties broken by the order the types appear in buildings.toml
|
||||||
|
// (which is the order of the build button bar, REQ-UI-BUILD-BAR). Building types absent
|
||||||
|
// from the config sort last. Derived display data for the blueprint card, kept here so
|
||||||
|
// it is unit-testable rather than buried in the dialog (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
std::vector<BlueprintContentEntry> summarizeBlueprintContents(
|
||||||
|
const Blueprint& blueprint, const BuildingsConfig& buildings);
|
||||||
|
|
||||||
|
// Plain sum of the placement cost of every building in the blueprint
|
||||||
|
// (REQ-UI-BLUEPRINT-CARD). Distinct from the total charged on placement
|
||||||
|
// (REQ-UI-BLUEPRINT-PLACE), which additionally excludes locked building types and
|
||||||
|
// rotate-in-place targets; see GameWorldView's placement path.
|
||||||
|
int computeBlueprintCost(const Blueprint& blueprint, const BuildingsConfig& buildings);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ static Rotation rotCCW(Rotation r)
|
|||||||
return Rotation::East;
|
return Rotation::East;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror of BlueprintPanel::createBlueprintFromSelection: given per-building
|
// Mirror of BlueprintLibrary::createBlueprintFromSelection: given per-building
|
||||||
// (anchor, bodyCells, type, rotation), compute Blueprint with floor-division
|
// (anchor, bodyCells, type, rotation), compute Blueprint with floor-division
|
||||||
// bounding-box center and per-building tile offsets.
|
// bounding-box center and per-building tile offsets.
|
||||||
struct BuildingSpec
|
struct BuildingSpec
|
||||||
@@ -149,7 +149,7 @@ static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter:
|
// Mirrors BlueprintLibrary::createBlueprintFromSelection's player-placeable filter:
|
||||||
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
|
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
|
||||||
// are silently excluded before the bounding-box center and offsets are computed.
|
// are silently excluded before the bounding-box center and offsets are computed.
|
||||||
static Blueprint buildBlueprintFiltered(const std::vector<BuildingSpec>& specs,
|
static Blueprint buildBlueprintFiltered(const std::vector<BuildingSpec>& specs,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ TEST_CASE("Only one mode is active at a time", "[buildmode]")
|
|||||||
TEST_CASE("Entering builder mode announces that blueprint mode ended", "[buildmode]")
|
TEST_CASE("Entering builder mode announces that blueprint mode ended", "[buildmode]")
|
||||||
{
|
{
|
||||||
// Regression: entering builder mode used to drop the blueprint silently, so the
|
// Regression: entering builder mode used to drop the blueprint silently, so the
|
||||||
// blueprint panel kept its button highlighted for a mode that was over.
|
// blueprint library kept tracking an active blueprint for a mode that was over.
|
||||||
BuildModeController controller;
|
BuildModeController controller;
|
||||||
controller.enterBlueprintMode(makeBlueprint());
|
controller.enterBlueprintMode(makeBlueprint());
|
||||||
|
|
||||||
|
|||||||
@@ -139,3 +139,100 @@ TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]"
|
|||||||
Simulation sim(loadTestConfig(), 7);
|
Simulation sim(loadTestConfig(), 7);
|
||||||
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The derived facts a blueprint card shows (REQ-UI-BLUEPRINT-CARD). They live here
|
||||||
|
// rather than in the dialog so they can be tested: src/ui is off the test include path.
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Blueprint makeBlueprint(const std::vector<BuildingType>& types)
|
||||||
|
{
|
||||||
|
Blueprint blueprint;
|
||||||
|
for (const BuildingType type : types)
|
||||||
|
{
|
||||||
|
BlueprintBuilding building;
|
||||||
|
building.type = type;
|
||||||
|
building.rotation = Rotation::East;
|
||||||
|
blueprint.buildings.push_back(building);
|
||||||
|
}
|
||||||
|
return blueprint;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("computeBlueprintCost sums the constituent placement costs", "[blueprint]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
|
// belt 2 + belt 2 + miner 15.
|
||||||
|
const Blueprint blueprint = makeBlueprint(
|
||||||
|
{BuildingType::Belt, BuildingType::Belt, BuildingType::Miner});
|
||||||
|
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("computeBlueprintCost ignores types absent from buildings.toml", "[blueprint]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
REQUIRE(cfg.buildings.findBuildingDef(BuildingType::Hq) == nullptr);
|
||||||
|
|
||||||
|
const Blueprint blueprint = makeBlueprint({BuildingType::Belt, BuildingType::Hq});
|
||||||
|
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("computeBlueprintCost is zero for an empty blueprint", "[blueprint]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
CHECK(computeBlueprintCost(Blueprint{}, cfg.buildings) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("summarizeBlueprintContents orders by descending count", "[blueprint]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
|
const Blueprint blueprint = makeBlueprint(
|
||||||
|
{BuildingType::Belt, BuildingType::Smelter, BuildingType::Miner,
|
||||||
|
BuildingType::Miner, BuildingType::Smelter, BuildingType::Miner});
|
||||||
|
|
||||||
|
const std::vector<BlueprintContentEntry> summary =
|
||||||
|
summarizeBlueprintContents(blueprint, cfg.buildings);
|
||||||
|
REQUIRE(summary.size() == 3);
|
||||||
|
CHECK(summary[0].buildingName == "Miner");
|
||||||
|
CHECK(summary[0].count == 3);
|
||||||
|
CHECK(summary[1].buildingName == "Smelter");
|
||||||
|
CHECK(summary[1].count == 2);
|
||||||
|
CHECK(summary[2].buildingName == "Belt");
|
||||||
|
CHECK(summary[2].count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("summarizeBlueprintContents breaks ties in buildings.toml order", "[blueprint]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
|
// Miner is declared after Belt in buildings.toml, which is the order the build
|
||||||
|
// button bar lays its buttons out, so Belt wins the tie despite the equal counts
|
||||||
|
// and despite sorting later by enum value.
|
||||||
|
const Blueprint blueprint = makeBlueprint(
|
||||||
|
{BuildingType::Miner, BuildingType::Belt, BuildingType::Miner, BuildingType::Belt});
|
||||||
|
|
||||||
|
const std::vector<BlueprintContentEntry> summary =
|
||||||
|
summarizeBlueprintContents(blueprint, cfg.buildings);
|
||||||
|
REQUIRE(summary.size() == 2);
|
||||||
|
CHECK(summary[0].buildingName == "Belt");
|
||||||
|
CHECK(summary[1].buildingName == "Miner");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("summarizeBlueprintContents spells multi-word ids as display names", "[blueprint]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
|
const Blueprint blueprint = makeBlueprint({BuildingType::TunnelEntry});
|
||||||
|
const std::vector<BlueprintContentEntry> summary =
|
||||||
|
summarizeBlueprintContents(blueprint, cfg.buildings);
|
||||||
|
REQUIRE(summary.size() == 1);
|
||||||
|
CHECK(summary[0].buildingName == "Tunnel Entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("summarizeBlueprintContents is empty for an empty blueprint", "[blueprint]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
CHECK(summarizeBlueprintContents(Blueprint{}, cfg.buildings).empty());
|
||||||
|
}
|
||||||
|
|||||||
175
src/ui/BlueprintLibrary.cpp
Normal file
175
src/ui/BlueprintLibrary.cpp
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#include "BlueprintLibrary.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
#include "BlueprintPlacementRequestedEvent.h"
|
||||||
|
#include "BlueprintSerializer.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "ExitBlueprintModeRequestedEvent.h"
|
||||||
|
|
||||||
|
#include "BuildingConfig.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
BlueprintLibrary::BlueprintLibrary(Simulation* sim, const GameConfig* config,
|
||||||
|
QWidget* dialogParent)
|
||||||
|
: m_sim(sim)
|
||||||
|
, m_config(config)
|
||||||
|
, m_dialogParent(dialogParent)
|
||||||
|
{
|
||||||
|
loadFromDisk();
|
||||||
|
registerForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
BlueprintLibrary::~BlueprintLibrary()
|
||||||
|
{
|
||||||
|
saveToDisk();
|
||||||
|
unregisterForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BlueprintLibrary::getCanCaptureSelection() const
|
||||||
|
{
|
||||||
|
// A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE).
|
||||||
|
return selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::saveSelectionAs(const QString& name)
|
||||||
|
{
|
||||||
|
Blueprint blueprint = createBlueprintFromSelection();
|
||||||
|
if (blueprint.buildings.empty()) { return; }
|
||||||
|
|
||||||
|
blueprint.name = name;
|
||||||
|
m_blueprints.push_back(std::move(blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Blueprint>& BlueprintLibrary::getBlueprints() const
|
||||||
|
{
|
||||||
|
return m_blueprints;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString BlueprintLibrary::getContentsSummary(int index) const
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return QString(); }
|
||||||
|
|
||||||
|
QStringList entries;
|
||||||
|
for (const BlueprintContentEntry& entry : summarizeBlueprintContents(
|
||||||
|
m_blueprints[static_cast<std::size_t>(index)], m_config->buildings))
|
||||||
|
{
|
||||||
|
// The same "<type> x <count>" notation the building multi-selection summary
|
||||||
|
// uses (REQ-UI-MULTI-SELECTION), which also sidesteps plural forms.
|
||||||
|
entries << QObject::tr("%1 x %2")
|
||||||
|
.arg(QString::fromStdString(entry.buildingName))
|
||||||
|
.arg(entry.count);
|
||||||
|
}
|
||||||
|
return entries.join(QStringLiteral(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
int BlueprintLibrary::getCost(int index) const
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return 0; }
|
||||||
|
return computeBlueprintCost(m_blueprints[static_cast<std::size_t>(index)],
|
||||||
|
m_config->buildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BlueprintLibrary::getCanAfford(int index) const
|
||||||
|
{
|
||||||
|
return m_sim->getBuildingBlocksStock() >= getCost(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::remove(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
|
||||||
|
|
||||||
|
if (m_activeIndex == index)
|
||||||
|
{
|
||||||
|
m_activeIndex = std::nullopt;
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||||
|
}
|
||||||
|
else if (m_activeIndex.has_value() && *m_activeIndex > index)
|
||||||
|
{
|
||||||
|
--*m_activeIndex;
|
||||||
|
}
|
||||||
|
m_blueprints.erase(m_blueprints.begin() + index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::beginPlacement(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
|
||||||
|
|
||||||
|
m_activeIndex = index;
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BlueprintPlacementRequestedEvent>(
|
||||||
|
m_blueprints[static_cast<std::size_t>(index)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
|
{
|
||||||
|
m_selectedBuildingIds = event->ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
m_activeIndex = std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::handleEvent(
|
||||||
|
std::shared_ptr<const TemporaryBlueprintRequestedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
// Temporary blueprint (REQ-UI-BLUEPRINT-TEMP): build from the current selection and
|
||||||
|
// enter placement mode without adding it to the list or persisting it. If nothing
|
||||||
|
// player-placeable is selected, do nothing.
|
||||||
|
Blueprint blueprint = createBlueprintFromSelection();
|
||||||
|
if (blueprint.buildings.empty()) { return; }
|
||||||
|
|
||||||
|
// No saved blueprint is active while a temporary one is being placed.
|
||||||
|
m_activeIndex = std::nullopt;
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BlueprintPlacementRequestedEvent>(std::move(blueprint)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Blueprint BlueprintLibrary::createBlueprintFromSelection() const
|
||||||
|
{
|
||||||
|
// Capture is shared, testable logic in lib/sim: it resolves each selected id as an
|
||||||
|
// operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE,
|
||||||
|
// REQ-UI-BLUEPRINT-STORAGE).
|
||||||
|
return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::loadFromDisk()
|
||||||
|
{
|
||||||
|
// Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error.
|
||||||
|
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString());
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
QMessageBox::critical(m_dialogParent, QObject::tr("Load Failed"),
|
||||||
|
QObject::tr("Failed to load blueprints:\n%1").arg(e.what()));
|
||||||
|
m_blueprints.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintLibrary::saveToDisk() const
|
||||||
|
{
|
||||||
|
// Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE).
|
||||||
|
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; }
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const std::string content = BlueprintSerializer::serialize(m_blueprints);
|
||||||
|
file.write(QByteArray::fromStdString(content));
|
||||||
|
}
|
||||||
|
catch (...) {}
|
||||||
|
}
|
||||||
86
src/ui/BlueprintLibrary.h
Normal file
86
src/ui/BlueprintLibrary.h
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "Blueprint.h"
|
||||||
|
#include "BlueprintModeExitedEvent.h"
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "EventHandler.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "SelectionChangedEvent.h"
|
||||||
|
#include "TemporaryBlueprintRequestedEvent.h"
|
||||||
|
|
||||||
|
class Simulation;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
// The player's saved blueprints: the list itself, its persistence (REQ-UI-BLUEPRINT-SAVE,
|
||||||
|
// REQ-UI-BLUEPRINT-LOAD), capture from the current selection (REQ-UI-BLUEPRINT-CREATE,
|
||||||
|
// REQ-UI-BLUEPRINT-TEMP), and entry into blueprint placement mode.
|
||||||
|
//
|
||||||
|
// Deliberately not a widget: blueprints have no permanent place on screen any more
|
||||||
|
// (REQ-UI-BLUEPRINT-DIALOG), and the modal dialogs that present them must be driven from
|
||||||
|
// MainWindow, the only widget that can pause the game (ModalPauseScope) and raise the dim
|
||||||
|
// overlay. This class is the model those dialogs read and mutate.
|
||||||
|
class BlueprintLibrary : public CombinedEventHandler<SelectionChangedEvent,
|
||||||
|
BlueprintModeExitedEvent,
|
||||||
|
TemporaryBlueprintRequestedEvent>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// dialogParent parents the load-failure message box (REQ-UI-BLUEPRINT-LOAD) and is
|
||||||
|
// not owned.
|
||||||
|
BlueprintLibrary(Simulation* sim, const GameConfig* config, QWidget* dialogParent);
|
||||||
|
~BlueprintLibrary();
|
||||||
|
|
||||||
|
// True when the current selection holds at least one player-placeable building or
|
||||||
|
// construction site -- the condition under which Ctrl+C has any effect
|
||||||
|
// (REQ-UI-BLUEPRINT-CREATE).
|
||||||
|
bool getCanCaptureSelection() const;
|
||||||
|
|
||||||
|
// Captures the current selection under the given name and appends it to the list.
|
||||||
|
// Silently does nothing when nothing player-placeable is selected.
|
||||||
|
void saveSelectionAs(const QString& name);
|
||||||
|
|
||||||
|
const std::vector<Blueprint>& getBlueprints() const;
|
||||||
|
|
||||||
|
// The blueprint's contents line, ready for its card: one "<name> x <count>" entry
|
||||||
|
// per building type, comma-separated, ordered by summarizeBlueprintContents
|
||||||
|
// (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
QString getContentsSummary(int index) const;
|
||||||
|
|
||||||
|
// Sum of the constituent buildings' placement costs (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
int getCost(int index) const;
|
||||||
|
|
||||||
|
// Whether the player can currently afford the blueprint's total cost, which is what
|
||||||
|
// enables or greys out its card (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
bool getCanAfford(int index) const;
|
||||||
|
|
||||||
|
// Removes the blueprint, exiting blueprint placement mode if it was the active one
|
||||||
|
// (REQ-UI-BLUEPRINT-DELETE).
|
||||||
|
void remove(int index);
|
||||||
|
|
||||||
|
// Enters blueprint placement mode for the blueprint (REQ-UI-BLUEPRINT-MODE).
|
||||||
|
void beginPlacement(int index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> event) override;
|
||||||
|
|
||||||
|
Blueprint createBlueprintFromSelection() const;
|
||||||
|
void loadFromDisk();
|
||||||
|
void saveToDisk() const;
|
||||||
|
|
||||||
|
// The simulation is the single source of truth for the block stock and the
|
||||||
|
// selection; the change events are only refresh signals.
|
||||||
|
Simulation* m_sim;
|
||||||
|
const GameConfig* m_config;
|
||||||
|
QWidget* m_dialogParent;
|
||||||
|
std::vector<BuildingId> m_selectedBuildingIds;
|
||||||
|
std::vector<Blueprint> m_blueprints;
|
||||||
|
// Index of the blueprint currently in placement mode, so deleting it can exit that
|
||||||
|
// mode (REQ-UI-BLUEPRINT-DELETE). nullopt = no saved blueprint is being placed.
|
||||||
|
std::optional<int> m_activeIndex;
|
||||||
|
};
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
#include "BlueprintPanel.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <climits>
|
|
||||||
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QInputDialog>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "BlueprintPlacementRequestedEvent.h"
|
|
||||||
#include "BlueprintSerializer.h"
|
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
|
||||||
#include "EventManager.h"
|
|
||||||
#include "ExitBlueprintModeRequestedEvent.h"
|
|
||||||
|
|
||||||
#include "Building.h"
|
|
||||||
#include "BuildingConfig.h"
|
|
||||||
#include "BuildingSystem.h"
|
|
||||||
#include "Simulation.h"
|
|
||||||
|
|
||||||
BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
, m_sim(sim)
|
|
||||||
, m_config(config)
|
|
||||||
{
|
|
||||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
|
||||||
layout->setContentsMargins(4, 4, 4, 4);
|
|
||||||
layout->setSpacing(4);
|
|
||||||
|
|
||||||
m_createBtn = new QPushButton(tr("Create Blueprint"), this);
|
|
||||||
m_createBtn->setFixedHeight(48);
|
|
||||||
m_createBtn->setEnabled(false);
|
|
||||||
layout->addWidget(m_createBtn);
|
|
||||||
|
|
||||||
QScrollArea* scrollArea = new QScrollArea(this);
|
|
||||||
scrollArea->setWidgetResizable(true);
|
|
||||||
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
||||||
|
|
||||||
m_buttonsContainer = new QWidget(scrollArea);
|
|
||||||
m_buttonsLayout = new QVBoxLayout(m_buttonsContainer);
|
|
||||||
m_buttonsLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
m_buttonsLayout->setSpacing(4);
|
|
||||||
m_buttonsLayout->addStretch();
|
|
||||||
|
|
||||||
scrollArea->setWidget(m_buttonsContainer);
|
|
||||||
layout->addWidget(scrollArea, 1);
|
|
||||||
|
|
||||||
connect(m_createBtn, &QPushButton::clicked, this, &BlueprintPanel::onCreateClicked);
|
|
||||||
|
|
||||||
loadFromDisk();
|
|
||||||
rebuildButtons();
|
|
||||||
|
|
||||||
registerForEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
BlueprintPanel::~BlueprintPanel()
|
|
||||||
{
|
|
||||||
saveToDisk();
|
|
||||||
unregisterForEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
|
|
||||||
{
|
|
||||||
m_selectedBuildingIds = ids;
|
|
||||||
refreshButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
refreshButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::clearActiveBlueprintButton()
|
|
||||||
{
|
|
||||||
if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
|
|
||||||
{
|
|
||||||
m_blueprintButtons[static_cast<std::size_t>(*m_activeIndex)]->setChecked(false);
|
|
||||||
}
|
|
||||||
m_activeIndex = std::nullopt;
|
|
||||||
refreshButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::onCreateClicked()
|
|
||||||
{
|
|
||||||
if (m_selectedBuildingIds.empty()) { return; }
|
|
||||||
|
|
||||||
Blueprint bp = createBlueprintFromSelection();
|
|
||||||
if (bp.buildings.empty()) { return; }
|
|
||||||
|
|
||||||
bool ok = false;
|
|
||||||
const QString name = QInputDialog::getText(
|
|
||||||
this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal, QString(), &ok);
|
|
||||||
if (!ok || name.trimmed().isEmpty()) { return; }
|
|
||||||
|
|
||||||
bp.name = name.trimmed();
|
|
||||||
m_blueprints.push_back(bp);
|
|
||||||
rebuildButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::onDeleteBlueprintClicked(int index)
|
|
||||||
{
|
|
||||||
if (m_activeIndex == index)
|
|
||||||
{
|
|
||||||
m_activeIndex = std::nullopt;
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
|
||||||
}
|
|
||||||
else if (m_activeIndex.has_value() && *m_activeIndex > index)
|
|
||||||
{
|
|
||||||
--*m_activeIndex;
|
|
||||||
}
|
|
||||||
m_blueprints.erase(m_blueprints.begin() + index);
|
|
||||||
rebuildButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::onBlueprintButtonClicked(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= static_cast<int>(m_blueprints.size())) { return; }
|
|
||||||
|
|
||||||
if (m_activeIndex == index)
|
|
||||||
{
|
|
||||||
clearActiveBlueprintButton();
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_activeIndex.has_value() && *m_activeIndex < static_cast<int>(m_blueprintButtons.size()))
|
|
||||||
{
|
|
||||||
m_blueprintButtons[static_cast<std::size_t>(*m_activeIndex)]->setChecked(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_activeIndex = index;
|
|
||||||
m_blueprintButtons[static_cast<std::size_t>(index)]->setChecked(true);
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<BlueprintPlacementRequestedEvent>(m_blueprints[static_cast<std::size_t>(index)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
|
||||||
{
|
|
||||||
// Capture is shared, testable logic in lib/sim: it resolves each selected id as an
|
|
||||||
// operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE,
|
|
||||||
// REQ-UI-BLUEPRINT-STORAGE).
|
|
||||||
return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const
|
|
||||||
{
|
|
||||||
int total = 0;
|
|
||||||
for (const BlueprintBuilding& bb : bp.buildings)
|
|
||||||
{
|
|
||||||
const BuildingDef* def = m_config->buildings.findBuildingDef(bb.type);
|
|
||||||
if (def) { total += def->cost; }
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::rebuildButtons()
|
|
||||||
{
|
|
||||||
while (m_buttonsLayout->count() > 1)
|
|
||||||
{
|
|
||||||
QLayoutItem* item = m_buttonsLayout->takeAt(0);
|
|
||||||
if (item->widget()) { delete item->widget(); }
|
|
||||||
delete item;
|
|
||||||
}
|
|
||||||
m_blueprintButtons.clear();
|
|
||||||
|
|
||||||
for (int i = 0; i < static_cast<int>(m_blueprints.size()); ++i)
|
|
||||||
{
|
|
||||||
const Blueprint& bp = m_blueprints[static_cast<std::size_t>(i)];
|
|
||||||
const int cost = computeBlueprintCost(bp);
|
|
||||||
const QString label = bp.name + "\n" + tr("%1 Building Blocks").arg(cost);
|
|
||||||
|
|
||||||
QWidget* row = new QWidget(m_buttonsContainer);
|
|
||||||
QHBoxLayout* rowLayout = new QHBoxLayout(row);
|
|
||||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
rowLayout->setSpacing(4);
|
|
||||||
|
|
||||||
QPushButton* btn = new QPushButton(label, row);
|
|
||||||
btn->setCheckable(true);
|
|
||||||
btn->setFixedHeight(48);
|
|
||||||
|
|
||||||
QPushButton* delBtn = new QPushButton("\xc3\x97", row);
|
|
||||||
delBtn->setFixedWidth(28);
|
|
||||||
delBtn->setFixedHeight(48);
|
|
||||||
|
|
||||||
rowLayout->addWidget(btn, 1);
|
|
||||||
rowLayout->addWidget(delBtn, 0);
|
|
||||||
|
|
||||||
m_buttonsLayout->insertWidget(i, row);
|
|
||||||
|
|
||||||
const int capturedIndex = i;
|
|
||||||
connect(btn, &QPushButton::clicked, this, [this, capturedIndex]() {
|
|
||||||
onBlueprintButtonClicked(capturedIndex);
|
|
||||||
});
|
|
||||||
connect(delBtn, &QPushButton::clicked, this, [this, capturedIndex]() {
|
|
||||||
onDeleteBlueprintClicked(capturedIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
m_blueprintButtons.push_back(btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshButtonStates();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::saveToDisk() const
|
|
||||||
{
|
|
||||||
// Persist on shutdown; write errors are silently ignored (REQ-UI-BLUEPRINT-SAVE).
|
|
||||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
|
||||||
QFile file(path);
|
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; }
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const std::string content = BlueprintSerializer::serialize(m_blueprints);
|
|
||||||
file.write(QByteArray::fromStdString(content));
|
|
||||||
}
|
|
||||||
catch (...) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::loadFromDisk()
|
|
||||||
{
|
|
||||||
// Load at startup (REQ-UI-BLUEPRINT-LOAD). Missing file: start empty, no error.
|
|
||||||
const QString path = QCoreApplication::applicationDirPath() + "/blueprints.toml";
|
|
||||||
QFile file(path);
|
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; }
|
|
||||||
try
|
|
||||||
{
|
|
||||||
m_blueprints = BlueprintSerializer::deserialize(file.readAll().toStdString());
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
QMessageBox::critical(this, tr("Load Failed"),
|
|
||||||
tr("Failed to load blueprints:\n%1").arg(e.what()));
|
|
||||||
m_blueprints.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::refreshButtonStates()
|
|
||||||
{
|
|
||||||
// A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE).
|
|
||||||
m_createBtn->setEnabled(selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds));
|
|
||||||
|
|
||||||
const int blocks = m_sim->getBuildingBlocksStock();
|
|
||||||
|
|
||||||
for (int i = 0; i < static_cast<int>(m_blueprintButtons.size()); ++i)
|
|
||||||
{
|
|
||||||
const int cost = computeBlueprintCost(m_blueprints[static_cast<std::size_t>(i)]);
|
|
||||||
const bool canAfford = blocks >= cost;
|
|
||||||
m_blueprintButtons[static_cast<std::size_t>(i)]->setEnabled(
|
|
||||||
canAfford || m_activeIndex == i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
|
||||||
{
|
|
||||||
onSelectionChanged(event->ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
clearActiveBlueprintButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BlueprintPanel::handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
// Temporary blueprint (REQ-UI-BLUEPRINT-TEMP): build from the current selection and
|
|
||||||
// enter placement mode without adding it to the list or persisting it. If nothing
|
|
||||||
// player-placeable is selected, do nothing.
|
|
||||||
Blueprint bp = createBlueprintFromSelection();
|
|
||||||
if (bp.buildings.empty()) { return; }
|
|
||||||
|
|
||||||
// No saved blueprint is active while a temporary one is being placed.
|
|
||||||
clearActiveBlueprintButton();
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<BlueprintPlacementRequestedEvent>(std::move(bp)));
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
#include "Blueprint.h"
|
|
||||||
#include "BlueprintModeExitedEvent.h"
|
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
|
||||||
#include "BuildingId.h"
|
|
||||||
#include "EventHandler.h"
|
|
||||||
#include "GameConfig.h"
|
|
||||||
#include "SelectionChangedEvent.h"
|
|
||||||
#include "TemporaryBlueprintRequestedEvent.h"
|
|
||||||
#include "Tick.h"
|
|
||||||
|
|
||||||
class Simulation;
|
|
||||||
class QPushButton;
|
|
||||||
class QScrollArea;
|
|
||||||
class QVBoxLayout;
|
|
||||||
|
|
||||||
class BlueprintPanel : public QWidget,
|
|
||||||
public CombinedEventHandler<BuildingBlocksChangedEvent,
|
|
||||||
SelectionChangedEvent,
|
|
||||||
BlueprintModeExitedEvent,
|
|
||||||
TemporaryBlueprintRequestedEvent>
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
|
|
||||||
~BlueprintPanel() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> event) override;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onCreateClicked();
|
|
||||||
void onDeleteBlueprintClicked(int index);
|
|
||||||
void onBlueprintButtonClicked(int index);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
|
||||||
void clearActiveBlueprintButton();
|
|
||||||
Blueprint createBlueprintFromSelection() const;
|
|
||||||
int computeBlueprintCost(const Blueprint& bp) const;
|
|
||||||
void rebuildButtons();
|
|
||||||
void refreshButtonStates();
|
|
||||||
void loadFromDisk();
|
|
||||||
void saveToDisk() const;
|
|
||||||
|
|
||||||
// The simulation is the single source of truth for the block stock; the
|
|
||||||
// change event is only a refresh signal.
|
|
||||||
Simulation* m_sim;
|
|
||||||
const GameConfig* m_config;
|
|
||||||
std::vector<BuildingId> m_selectedBuildingIds;
|
|
||||||
std::optional<int> m_activeIndex; // nullopt = no blueprint selected
|
|
||||||
std::vector<Blueprint> m_blueprints;
|
|
||||||
std::vector<QPushButton*> m_blueprintButtons;
|
|
||||||
QPushButton* m_createBtn;
|
|
||||||
QWidget* m_buttonsContainer;
|
|
||||||
QVBoxLayout* m_buttonsLayout;
|
|
||||||
};
|
|
||||||
333
src/ui/BlueprintSelectionDialog.cpp
Normal file
333
src/ui/BlueprintSelectionDialog.cpp
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
#include "BlueprintSelectionDialog.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QChar>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLayoutItem>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Blueprint.h"
|
||||||
|
#include "BlueprintLibrary.h"
|
||||||
|
#include "IconCaption.h"
|
||||||
|
#include "ItemIconCache.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Fixed at two, per REQ-UI-BLUEPRINT-DIALOG; the grid scrolls rather than reflowing.
|
||||||
|
const int kGridColumnCount = 2;
|
||||||
|
|
||||||
|
// Card geometry. Every card is the same size so the grid stays even
|
||||||
|
// (REQ-UI-BLUEPRINT-CARD); the width is chosen to fit a name and a short contents
|
||||||
|
// line, and the height follows from three text rows.
|
||||||
|
const int kCardWidthPx = 220;
|
||||||
|
const int kCardPaddingPx = 10;
|
||||||
|
const int kCardRowGapPx = 4;
|
||||||
|
|
||||||
|
// Side length of the per-card delete icon and of the dialog's close button.
|
||||||
|
const int kSmallButtonSizePx = 22;
|
||||||
|
|
||||||
|
// Spacing between cards, and the dialog's own content margin.
|
||||||
|
const int kSpacingPx = 8;
|
||||||
|
|
||||||
|
// Card rows visible before the grid scrolls. The half row is deliberate: a cut-off
|
||||||
|
// card reads as "there is more below" at a glance.
|
||||||
|
const double kVisibleRowCount = 2.5;
|
||||||
|
|
||||||
|
// U+00D7 MULTIPLICATION SIGN, the close and delete glyph. Written as a code point
|
||||||
|
// because the sources are not guaranteed to be read as UTF-8 by every compiler.
|
||||||
|
const QChar kCrossGlyph(0x00D7);
|
||||||
|
|
||||||
|
// Inner size of a card's composed face: the name row, the contents row, and the
|
||||||
|
// cost row, plus the gap above the cost.
|
||||||
|
QSize getCardFaceSize(const QFont& font)
|
||||||
|
{
|
||||||
|
const int rowHeight = QFontMetrics(font).height();
|
||||||
|
return QSize(kCardWidthPx - 2 * kCardPaddingPx, rowHeight * 3 + kCardRowGapPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize getCardSize(const QFont& font)
|
||||||
|
{
|
||||||
|
const QSize faceSize = getCardFaceSize(font);
|
||||||
|
return QSize(faceSize.width() + 2 * kCardPaddingPx,
|
||||||
|
faceSize.height() + 2 * kCardPaddingPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One card face: the blueprint name, its contents line below in a dimmed color,
|
||||||
|
// and the cost with its block icon at the bottom (REQ-UI-BLUEPRINT-CARD). The three
|
||||||
|
// are composed into a single pixmap because a QPushButton holds only one icon --
|
||||||
|
// the same device the build button faces use.
|
||||||
|
QPixmap composeCardFace(const QString& name, const QString& contents,
|
||||||
|
const QPixmap& cost, const QFont& font,
|
||||||
|
const QColor& textColor, const QColor& dimColor,
|
||||||
|
const QSize& faceSize)
|
||||||
|
{
|
||||||
|
const QFontMetrics metrics(font);
|
||||||
|
const int rowHeight = metrics.height();
|
||||||
|
|
||||||
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
QPixmap face(static_cast<int>(faceSize.width() * dpr),
|
||||||
|
static_cast<int>(faceSize.height() * dpr));
|
||||||
|
face.setDevicePixelRatio(dpr);
|
||||||
|
face.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&face);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
painter.setFont(font);
|
||||||
|
|
||||||
|
// Both text rows are elided rather than wrapped or shrunk, so a long name or a
|
||||||
|
// many-typed blueprint cannot change a card's height (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
painter.setPen(textColor);
|
||||||
|
painter.drawText(QRect(0, 0, faceSize.width(), rowHeight),
|
||||||
|
Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
metrics.elidedText(name, Qt::ElideRight, faceSize.width()));
|
||||||
|
|
||||||
|
painter.setPen(dimColor);
|
||||||
|
painter.drawText(QRect(0, rowHeight, faceSize.width(), rowHeight),
|
||||||
|
Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
metrics.elidedText(contents, Qt::ElideRight, faceSize.width()));
|
||||||
|
|
||||||
|
const QSize costSize = getLogicalSize(cost);
|
||||||
|
painter.drawPixmap(0, faceSize.height() - costSize.height(), cost);
|
||||||
|
return face;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A composed card face and the size to show it at; the button needs both, and only
|
||||||
|
// the composer knows the size it arrived at.
|
||||||
|
struct CardFace { QIcon icon; QSize size; };
|
||||||
|
|
||||||
|
// The two-mode face of one card. The modes differ only in the text color, so an
|
||||||
|
// unaffordable card greys itself when Qt swaps the pixmap on the disabled button
|
||||||
|
// (REQ-UI-BLUEPRINT-CARD, consistent with REQ-UI-BUILD-DISABLED).
|
||||||
|
CardFace buildCardFace(const QString& name, const QString& contents, int cost,
|
||||||
|
const QPixmap& blockIcon, const QFont& font,
|
||||||
|
const QPalette& palette)
|
||||||
|
{
|
||||||
|
const QSize faceSize = getCardFaceSize(font);
|
||||||
|
const QColor dimColor = palette.color(QPalette::Disabled, QPalette::ButtonText);
|
||||||
|
|
||||||
|
CardFace result;
|
||||||
|
for (QIcon::Mode mode : { QIcon::Normal, QIcon::Disabled })
|
||||||
|
{
|
||||||
|
const QColor textColor = palette.color(
|
||||||
|
(mode == QIcon::Normal) ? QPalette::Active : QPalette::Disabled,
|
||||||
|
QPalette::ButtonText);
|
||||||
|
// The cost reads exactly like the header stock and the build button costs:
|
||||||
|
// the number, then the block icon in place of a trailing "Blocks"
|
||||||
|
// (REQ-UI-BLOCKS-ICON). A null icon leaves the bare number.
|
||||||
|
const QPixmap costPixmap =
|
||||||
|
renderCaptionWithIcon(QString::number(cost), blockIcon, font, textColor);
|
||||||
|
const QPixmap face = composeCardFace(name, contents, costPixmap, font,
|
||||||
|
textColor, dimColor, faceSize);
|
||||||
|
result.icon.addPixmap(face, mode);
|
||||||
|
result.size = result.size.expandedTo(getLogicalSize(face));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library,
|
||||||
|
ItemIconCache* itemIcons,
|
||||||
|
QWidget* parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
, m_library(library)
|
||||||
|
, m_itemIcons(itemIcons)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Blueprints"));
|
||||||
|
setModal(true);
|
||||||
|
// 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.
|
||||||
|
setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
|
||||||
|
setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
setStyleSheet(QStringLiteral(
|
||||||
|
"BlueprintSelectionDialog { background-color: palette(window);"
|
||||||
|
" border: 1px solid palette(mid); }"));
|
||||||
|
|
||||||
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||||
|
mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx);
|
||||||
|
mainLayout->setSpacing(kSpacingPx);
|
||||||
|
|
||||||
|
QHBoxLayout* headerLayout = new QHBoxLayout();
|
||||||
|
headerLayout->setSpacing(kSpacingPx);
|
||||||
|
|
||||||
|
QLabel* titleLabel = new QLabel(tr("Blueprints"), this);
|
||||||
|
QFont headerFont = titleLabel->font();
|
||||||
|
headerFont.setBold(true);
|
||||||
|
titleLabel->setFont(headerFont);
|
||||||
|
headerLayout->addWidget(titleLabel);
|
||||||
|
|
||||||
|
// Dimmed and bold, the treatment the build button hotkey badges use, so the
|
||||||
|
// shortcut reads as a reminder rather than a second title (REQ-UI-BUILD-COST).
|
||||||
|
QLabel* hotkeyBadge = new QLabel(tr("Ctrl+V"), this);
|
||||||
|
hotkeyBadge->setFont(headerFont);
|
||||||
|
QPalette badgePalette = hotkeyBadge->palette();
|
||||||
|
badgePalette.setColor(hotkeyBadge->foregroundRole(),
|
||||||
|
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||||
|
hotkeyBadge->setPalette(badgePalette);
|
||||||
|
headerLayout->addWidget(hotkeyBadge);
|
||||||
|
|
||||||
|
headerLayout->addStretch();
|
||||||
|
|
||||||
|
QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this);
|
||||||
|
closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||||
|
closeButton->setToolTip(tr("Close"));
|
||||||
|
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
headerLayout->addWidget(closeButton);
|
||||||
|
|
||||||
|
mainLayout->addLayout(headerLayout);
|
||||||
|
|
||||||
|
QScrollArea* scrollArea = new QScrollArea(this);
|
||||||
|
scrollArea->setWidgetResizable(true);
|
||||||
|
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||||
|
|
||||||
|
m_gridContainer = new QWidget(scrollArea);
|
||||||
|
m_grid = new QGridLayout(m_gridContainer);
|
||||||
|
m_grid->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_grid->setSpacing(kSpacingPx);
|
||||||
|
m_grid->setAlignment(Qt::AlignTop | Qt::AlignLeft);
|
||||||
|
scrollArea->setWidget(m_gridContainer);
|
||||||
|
mainLayout->addWidget(scrollArea, 1);
|
||||||
|
|
||||||
|
rebuildGrid();
|
||||||
|
|
||||||
|
// Fixed size (REQ-UI-BLUEPRINT-DIALOG), sized to show kVisibleRowCount card rows.
|
||||||
|
const QSize cardSize = getCardSize(font());
|
||||||
|
const int gridWidth = kGridColumnCount * cardSize.width()
|
||||||
|
+ (kGridColumnCount - 1) * kSpacingPx;
|
||||||
|
const int gridHeight = static_cast<int>(
|
||||||
|
cardSize.height() * kVisibleRowCount + kSpacingPx * (kVisibleRowCount - 1.0));
|
||||||
|
setFixedSize(gridWidth + style()->pixelMetric(QStyle::PM_ScrollBarExtent)
|
||||||
|
+ 2 * kSpacingPx,
|
||||||
|
gridHeight + kSmallButtonSizePx + 3 * kSpacingPx);
|
||||||
|
|
||||||
|
// A frameless dialog does not get Qt's automatic centering on its parent.
|
||||||
|
if (parent)
|
||||||
|
{
|
||||||
|
const QRect parentRect = parent->window()->geometry();
|
||||||
|
move(parentRect.center() - QPoint(width() / 2, height() / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> BlueprintSelectionDialog::getChosenIndex() const
|
||||||
|
{
|
||||||
|
return m_chosenIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintSelectionDialog::rebuildGrid()
|
||||||
|
{
|
||||||
|
// deleteLater, not delete: this runs from a delete button's own clicked signal, and
|
||||||
|
// destroying that button inside its emission would leave Qt holding a dangling
|
||||||
|
// sender. hide() makes the removal immediate all the same.
|
||||||
|
while (QLayoutItem* item = m_grid->takeAt(0))
|
||||||
|
{
|
||||||
|
if (item->widget())
|
||||||
|
{
|
||||||
|
item->widget()->hide();
|
||||||
|
item->widget()->deleteLater();
|
||||||
|
}
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Blueprint>& blueprints = m_library->getBlueprints();
|
||||||
|
if (blueprints.empty())
|
||||||
|
{
|
||||||
|
QLabel* emptyLabel = new QLabel(
|
||||||
|
tr("No blueprints yet.\n\nSelect one or more buildings and press Ctrl+C "
|
||||||
|
"to save them as a blueprint."),
|
||||||
|
m_gridContainer);
|
||||||
|
emptyLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
emptyLabel->setWordWrap(true);
|
||||||
|
m_grid->addWidget(emptyLabel, 0, 0, 1, kGridColumnCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block icon shown to the right of each cost (REQ-UI-BLUEPRINT-CARD); null when no
|
||||||
|
// building_block icon exists, in which case the cost is the bare number.
|
||||||
|
const QPixmap blockIcon = m_itemIcons->hasIcon(kBlockItemId)
|
||||||
|
? m_itemIcons->getPixmap(kBlockItemId, QFontMetrics(font()).height())
|
||||||
|
: QPixmap();
|
||||||
|
|
||||||
|
const QSize cardSize = getCardSize(font());
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(blueprints.size()); ++i)
|
||||||
|
{
|
||||||
|
const Blueprint& blueprint = blueprints[static_cast<std::size_t>(i)];
|
||||||
|
const CardFace face = buildCardFace(
|
||||||
|
blueprint.name, m_library->getContentsSummary(i), m_library->getCost(i),
|
||||||
|
blockIcon, font(), palette());
|
||||||
|
|
||||||
|
QWidget* card = new QWidget(m_gridContainer);
|
||||||
|
card->setFixedSize(cardSize);
|
||||||
|
|
||||||
|
QVBoxLayout* cardLayout = new QVBoxLayout(card);
|
||||||
|
cardLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
QPushButton* body = new QPushButton(card);
|
||||||
|
body->setIcon(face.icon);
|
||||||
|
body->setIconSize(face.size);
|
||||||
|
// An unaffordable card is greyed and does not respond, but its delete icon
|
||||||
|
// stays live (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
body->setEnabled(m_library->getCanAfford(i));
|
||||||
|
cardLayout->addWidget(body);
|
||||||
|
|
||||||
|
// A sibling of the body rather than one of its children, and outside the
|
||||||
|
// layout: Qt disables a widget's children along with it, and the delete icon
|
||||||
|
// must stay enabled on an unaffordable card (REQ-UI-BLUEPRINT-DELETE).
|
||||||
|
QPushButton* deleteButton = new QPushButton(QString(kCrossGlyph), card);
|
||||||
|
deleteButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||||
|
deleteButton->setToolTip(tr("Delete blueprint"));
|
||||||
|
deleteButton->move(cardSize.width() - kSmallButtonSizePx - kCardPaddingPx / 2,
|
||||||
|
cardSize.height() - kSmallButtonSizePx - kCardPaddingPx / 2);
|
||||||
|
deleteButton->raise();
|
||||||
|
|
||||||
|
m_grid->addWidget(card, i / kGridColumnCount, i % kGridColumnCount);
|
||||||
|
|
||||||
|
const int index = i;
|
||||||
|
connect(body, &QPushButton::clicked, this, [this, index]()
|
||||||
|
{
|
||||||
|
onCardClicked(index);
|
||||||
|
});
|
||||||
|
connect(deleteButton, &QPushButton::clicked, this, [this, index]()
|
||||||
|
{
|
||||||
|
onDeleteClicked(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintSelectionDialog::onCardClicked(int index)
|
||||||
|
{
|
||||||
|
// Placement mode is entered by the caller once the dialog has closed
|
||||||
|
// (REQ-UI-BLUEPRINT-CARD).
|
||||||
|
m_chosenIndex = index;
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintSelectionDialog::onDeleteClicked(int index)
|
||||||
|
{
|
||||||
|
// No confirmation prompt, and the dialog stays open (REQ-UI-BLUEPRINT-DELETE).
|
||||||
|
m_library->remove(index);
|
||||||
|
rebuildGrid();
|
||||||
|
}
|
||||||
42
src/ui/BlueprintSelectionDialog.h
Normal file
42
src/ui/BlueprintSelectionDialog.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
class BlueprintLibrary;
|
||||||
|
class ItemIconCache;
|
||||||
|
class QGridLayout;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
// The blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG): a frameless modal panel
|
||||||
|
// showing every saved blueprint as a card in a scrolling two-column grid. The caller
|
||||||
|
// pauses the game and raises the dim overlay while it is open.
|
||||||
|
//
|
||||||
|
// Clicking a card accepts the dialog and reports that blueprint's index; the caller
|
||||||
|
// enters placement mode afterwards, so the dialog is already closed by then
|
||||||
|
// (REQ-UI-BLUEPRINT-CARD). Deleting acts on the library immediately and leaves the
|
||||||
|
// dialog open (REQ-UI-BLUEPRINT-DELETE). Escape and the close button dismiss it with
|
||||||
|
// no other effect.
|
||||||
|
class BlueprintSelectionDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Neither the library nor the icon cache is owned; both outlive the dialog.
|
||||||
|
BlueprintSelectionDialog(BlueprintLibrary* library, ItemIconCache* itemIcons,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
std::optional<int> getChosenIndex() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuildGrid();
|
||||||
|
void onCardClicked(int index);
|
||||||
|
void onDeleteClicked(int index);
|
||||||
|
|
||||||
|
BlueprintLibrary* m_library;
|
||||||
|
ItemIconCache* m_itemIcons;
|
||||||
|
QWidget* m_gridContainer;
|
||||||
|
QGridLayout* m_grid;
|
||||||
|
std::optional<int> m_chosenIndex; // nullopt = dismissed without picking
|
||||||
|
};
|
||||||
@@ -96,16 +96,6 @@ namespace
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A pixmap's size in device-independent pixels. The pixmaps composed here are
|
|
||||||
// rasterized at the device pixel ratio, so their raw size is not their layout size.
|
|
||||||
QSize getLogicalSize(const QPixmap& pixmap)
|
|
||||||
{
|
|
||||||
if (pixmap.isNull()) { return QSize(0, 0); }
|
|
||||||
const qreal dpr = pixmap.devicePixelRatio();
|
|
||||||
return QSize(static_cast<int>(pixmap.width() / dpr),
|
|
||||||
static_cast<int>(pixmap.height() / dpr));
|
|
||||||
}
|
|
||||||
|
|
||||||
// One button face: the hotkey badge in the top-left corner, the chip icon centered
|
// One button face: the hotkey badge in the top-left corner, the chip icon centered
|
||||||
// below it, and the caption — the cost, or the Deconstruct name — centered at the
|
// below it, and the caption — the cost, or the Deconstruct name — centered at the
|
||||||
// bottom (REQ-UI-BUILD-COST). The three are composed into a single pixmap because
|
// bottom (REQ-UI-BUILD-COST). The three are composed into a single pixmap because
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h
|
||||||
@@ -38,7 +39,8 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp
|
||||||
|
|||||||
@@ -46,3 +46,11 @@ QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
|||||||
|
|
||||||
return pixmap;
|
return pixmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QSize getLogicalSize(const QPixmap& pixmap)
|
||||||
|
{
|
||||||
|
if (pixmap.isNull()) { return QSize(0, 0); }
|
||||||
|
const qreal dpr = pixmap.devicePixelRatio();
|
||||||
|
return QSize(static_cast<int>(pixmap.width() / dpr),
|
||||||
|
static_cast<int>(pixmap.height() / dpr));
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
// Renders `text` followed by `icon` to its right, vertically centered, onto a
|
// Renders `text` followed by `icon` to its right, vertically centered, onto a
|
||||||
@@ -15,6 +16,11 @@
|
|||||||
QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
||||||
const QFont& font, const QColor& textColor);
|
const QFont& font, const QColor& textColor);
|
||||||
|
|
||||||
|
// A pixmap's size in device-independent pixels. The pixmaps composed by
|
||||||
|
// renderCaptionWithIcon and its callers are rasterized at the device pixel ratio, so
|
||||||
|
// their raw size is not the size to lay them out at.
|
||||||
|
QSize getLogicalSize(const QPixmap& pixmap);
|
||||||
|
|
||||||
// Item id of the building blocks resource, whose icon stands in for the "Blocks"
|
// Item id of the building blocks resource, whose icon stands in for the "Blocks"
|
||||||
// word wherever a cost or stock is captioned (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST,
|
// word wherever a cost or stock is captioned (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST,
|
||||||
// REQ-UI-EXPAND-BUTTON). It lives next to the caption helper because every caller of
|
// REQ-UI-EXPAND-BUTTON). It lives next to the caption helper because every caller of
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
|
|
||||||
|
#include "BlueprintSaveRequestedEvent.h"
|
||||||
|
#include "BlueprintSelectionRequestedEvent.h"
|
||||||
#include "BuildHotkeyPressedEvent.h"
|
#include "BuildHotkeyPressedEvent.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "DebugDrawToggleRequestedEvent.h"
|
#include "DebugDrawToggleRequestedEvent.h"
|
||||||
@@ -97,6 +99,27 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blueprint chords (REQ-UI-HOTKEYS). Checked ahead of the plain-key switch below,
|
||||||
|
// which binds bare A/D/W/S/R/Q/T and must not fire on a Ctrl chord. Both requests
|
||||||
|
// are decided by MainWindow, the only widget that can pause the game and dim the
|
||||||
|
// window for a modal.
|
||||||
|
if ((event->modifiers() & Qt::ControlModifier) != 0)
|
||||||
|
{
|
||||||
|
switch (event->key())
|
||||||
|
{
|
||||||
|
case Qt::Key_C:
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BlueprintSaveRequestedEvent>());
|
||||||
|
return true;
|
||||||
|
case Qt::Key_V:
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BlueprintSelectionRequestedEvent>());
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (event->key())
|
switch (event->key())
|
||||||
{
|
{
|
||||||
case Qt::Key_A:
|
case Qt::Key_A:
|
||||||
@@ -131,7 +154,7 @@ bool InputMapper::handleKeyPress(QKeyEvent* event)
|
|||||||
return true;
|
return true;
|
||||||
case Qt::Key_T:
|
case Qt::Key_T:
|
||||||
// Request a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
|
// Request a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
|
||||||
// The BlueprintPanel owns the selection and blueprint-capture logic; it decides
|
// The BlueprintLibrary owns the selection and blueprint-capture logic; it decides
|
||||||
// whether anything placeable is selected and drives placement mode from there.
|
// whether anything placeable is selected and drives placement mode from there.
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<TemporaryBlueprintRequestedEvent>());
|
std::make_shared<TemporaryBlueprintRequestedEvent>());
|
||||||
|
|||||||
@@ -9,12 +9,15 @@
|
|||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QInputDialog>
|
||||||
|
#include <QLineEdit>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "BlueprintPanel.h"
|
#include "BlueprintLibrary.h"
|
||||||
|
#include "BlueprintSelectionDialog.h"
|
||||||
#include "BuildButtonBar.h"
|
#include "BuildButtonBar.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
@@ -71,31 +74,29 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(), iconDir,
|
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(), iconDir,
|
||||||
m_itemIcons.get(), this);
|
m_itemIcons.get(), this);
|
||||||
|
|
||||||
|
// The blueprints have no widget of their own: they are saved with Ctrl+C and picked
|
||||||
|
// from a modal dialog (REQ-UI-BLUEPRINT-DIALOG), both driven from this window
|
||||||
|
// because only it can pause the game and raise the dim overlay. Built after the
|
||||||
|
// 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);
|
m_sidePanel = new QWidget(this);
|
||||||
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||||
sideLayout->setContentsMargins(1, 1, 1, 1);
|
sideLayout->setContentsMargins(1, 1, 1, 1);
|
||||||
sideLayout->setSpacing(1);
|
sideLayout->setSpacing(1);
|
||||||
|
|
||||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
// The selected building panel is the column's only panel and fills its height
|
||||||
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
|
|
||||||
|
|
||||||
// Equal stretch gives the two panels half the column height each
|
|
||||||
// (REQ-UI-PANEL-COLUMN).
|
// (REQ-UI-PANEL-COLUMN).
|
||||||
|
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||||
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||||
sideLayout->addWidget(m_blueprintPanel, 1);
|
|
||||||
|
|
||||||
// Draw a thin border around each of the two side-panel sections. The class
|
// Draw a thin border around the side panel section. The class scoped selector keeps
|
||||||
// scoped selectors keep the border on the panels themselves rather than
|
// the border on the panel itself rather than cascading onto its child widgets;
|
||||||
// cascading onto their child widgets; WA_StyledBackground lets the plain
|
// WA_StyledBackground lets the plain QWidget subclass honor the stylesheet box
|
||||||
// QWidget subclasses honor the stylesheet box (border/background).
|
// (border/background).
|
||||||
for (QWidget* panel : { static_cast<QWidget*>(m_selectedBuildingPanel),
|
m_selectedBuildingPanel->setAttribute(Qt::WA_StyledBackground, true);
|
||||||
static_cast<QWidget*>(m_blueprintPanel) })
|
|
||||||
{
|
|
||||||
panel->setAttribute(Qt::WA_StyledBackground, true);
|
|
||||||
}
|
|
||||||
m_sidePanel->setStyleSheet(QStringLiteral(
|
m_sidePanel->setStyleSheet(QStringLiteral(
|
||||||
"SelectedBuildingPanel, BlueprintPanel {"
|
"SelectedBuildingPanel { border: 1px solid palette(mid); }"));
|
||||||
" border: 1px solid palette(mid); }"));
|
|
||||||
|
|
||||||
// Created last so it stacks above the other children; covers the whole window and
|
// 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).
|
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
||||||
@@ -369,6 +370,47 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
// Ctrl+C has effect only when something player-placeable is selected; otherwise no
|
||||||
|
// dialog opens at all (REQ-UI-BLUEPRINT-CREATE).
|
||||||
|
if (!m_blueprintLibrary->getCanCaptureSelection()) { return; }
|
||||||
|
|
||||||
|
// Both scopes are held across the naming dialog and the selection dialog it hands
|
||||||
|
// over to, so the dim never blinks off and the simulation is not resumed in between
|
||||||
|
// (REQ-UI-MODAL-DIM).
|
||||||
|
ModalPauseScope pause(*m_gameWorldView);
|
||||||
|
ModalDimScope dim(*m_dimOverlay);
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
const QString name = QInputDialog::getText(
|
||||||
|
this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal,
|
||||||
|
QString(), &ok);
|
||||||
|
// Cancel, Escape, or an empty name: no blueprint, and no selection dialog.
|
||||||
|
if (!ok || name.trimmed().isEmpty()) { return; }
|
||||||
|
|
||||||
|
m_blueprintLibrary->saveSelectionAs(name.trimmed());
|
||||||
|
showBlueprintSelectionDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
ModalPauseScope pause(*m_gameWorldView);
|
||||||
|
ModalDimScope dim(*m_dimOverlay);
|
||||||
|
showBlueprintSelectionDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::showBlueprintSelectionDialog()
|
||||||
|
{
|
||||||
|
BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), this);
|
||||||
|
if (dialog.exec() == QDialog::Accepted && dialog.getChosenIndex().has_value())
|
||||||
|
{
|
||||||
|
// Entered after the dialog has closed, which is the order REQ-UI-BLUEPRINT-CARD
|
||||||
|
// describes: clicking a card closes the dialog and enters placement mode.
|
||||||
|
m_blueprintLibrary->beginPlacement(*dialog.getChosenIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||||
{
|
{
|
||||||
const Tick tick = m_sim->getCurrentTick();
|
const Tick tick = m_sim->getCurrentTick();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "BlueprintSaveRequestedEvent.h"
|
||||||
|
#include "BlueprintSelectionRequestedEvent.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "EscapeMenuRequestedEvent.h"
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
@@ -28,7 +30,7 @@ class GameWorldView;
|
|||||||
class HeaderBar;
|
class HeaderBar;
|
||||||
class SelectedBuildingPanel;
|
class SelectedBuildingPanel;
|
||||||
class BuildButtonBar;
|
class BuildButtonBar;
|
||||||
class BlueprintPanel;
|
class BlueprintLibrary;
|
||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
class QCloseEvent;
|
class QCloseEvent;
|
||||||
class QResizeEvent;
|
class QResizeEvent;
|
||||||
@@ -39,7 +41,9 @@ class MainWindow : public QWidget,
|
|||||||
WinEvent,
|
WinEvent,
|
||||||
EscapeMenuRequestedEvent,
|
EscapeMenuRequestedEvent,
|
||||||
LayoutDialogRequestedEvent,
|
LayoutDialogRequestedEvent,
|
||||||
RecipeSelectionRequestedEvent>
|
RecipeSelectionRequestedEvent,
|
||||||
|
BlueprintSaveRequestedEvent,
|
||||||
|
BlueprintSelectionRequestedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -59,6 +63,8 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
||||||
|
|
||||||
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
||||||
// by every restart path. On success the reloaded visuals are applied to this
|
// by every restart path. On success the reloaded visuals are applied to this
|
||||||
@@ -71,6 +77,12 @@ private:
|
|||||||
void openShipLayoutDialog(BuildingId shipyardId,
|
void openShipLayoutDialog(BuildingId shipyardId,
|
||||||
const std::string& schematicId,
|
const std::string& schematicId,
|
||||||
const ShipLayoutConfig& currentLayout);
|
const ShipLayoutConfig& currentLayout);
|
||||||
|
|
||||||
|
// Runs the blueprint selection dialog and enters placement mode for whatever the
|
||||||
|
// player picked (REQ-UI-BLUEPRINT-DIALOG). Holds no pause or dim scope of its own:
|
||||||
|
// both callers already hold theirs, which is what keeps the dim continuous when a
|
||||||
|
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
||||||
|
void showBlueprintSelectionDialog();
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -84,7 +96,9 @@ private:
|
|||||||
HeaderBar* m_headerBar;
|
HeaderBar* m_headerBar;
|
||||||
SelectedBuildingPanel* m_selectedBuildingPanel;
|
SelectedBuildingPanel* m_selectedBuildingPanel;
|
||||||
BuildButtonBar* m_buildButtonBar;
|
BuildButtonBar* m_buildButtonBar;
|
||||||
BlueprintPanel* m_blueprintPanel;
|
// 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;
|
QWidget* m_sidePanel;
|
||||||
ModalDimOverlay* m_dimOverlay = nullptr;
|
ModalDimOverlay* m_dimOverlay = nullptr;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user