Compare commits
6 Commits
37899ea964
...
ui_changes
| Author | SHA1 | Date | |
|---|---|---|---|
| aeab16757f | |||
| b97347329f | |||
| 352beda47c | |||
| 256c4b5492 | |||
| 997cbc65d3 | |||
| e66eb7a81f |
@@ -571,12 +571,12 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
|
|||||||
- an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one;
|
- an **input** chip shows the per-cycle amount below the count (the items consumed per run, e.g. `/ 2 per cycle`), or the count alone when the building has no selected recipe or schematic to give one;
|
||||||
- an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below.
|
- an **output** chip shows the count against the output buffer's capacity as `a / b` (REQ-MAT-OUTPUT-BUFFER), with the item's name below.
|
||||||
|
|
||||||
Input and output chips form separately captioned sections, each shown only while it holds something (REQ-UI-SELECTION-CARD). The production section (REQ-UI-PRODUCTION-PROGRESS) sits **between them**, so the card reads in the direction the materials flow: what goes in, what is being made of it, what has come out. For a selected construction site the buffer sections are omitted (REQ-BLD-SITE-CONFIG).
|
Input and output chips form separately captioned sections (REQ-UI-SELECTION-CARD). A section lists a chip for **every item the building's cycle involves**, and for an auto-recipe building every item it handles at all, whether or not the buffer currently holds any: an empty buffer reads `0` rather than its chip disappearing, so the card keeps one shape while the building runs. A section left with no chips at all is not shown. The production section (REQ-UI-PRODUCTION-PROGRESS) sits **between them**, so the card reads in the direction the materials flow: what goes in, what is being made of it, what has come out. For a selected construction site the buffer sections are omitted (REQ-BLD-SITE-CONFIG).
|
||||||
|
|
||||||
**Only unlocked items are listed.** A building's buffers may carry entries for items the player cannot make yet — an auto-recipe building's buffers are sized over *every* recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), including recipes that are still locked. Those entries are left out of both sections, consistent with the rest of the UI hiding what is not unlocked yet (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SPLITTER), so a Smelter shows the ores it can actually smelt rather than every ore in the game.
|
**Only unlocked items are listed.** A building's buffers may carry entries for items the player cannot make yet — an auto-recipe building's buffers are sized over *every* recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), including recipes that are still locked. Those entries are left out of both sections, consistent with the rest of the UI hiding what is not unlocked yet (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SPLITTER), so a Smelter shows the ores it can actually smelt rather than every ore in the game.
|
||||||
|
|
||||||
**An idle auto-recipe building still shows what it handles.** Having no selected recipe (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), it would otherwise show empty sections whenever it happens to be between cycles. Its input and output sections instead list the unlocked items of every recipe of its type — the same union its buffers were sized over — with a count and no per-cycle denominator, since no one recipe is in force. While a cycle is running, that cycle's recipe supplies the denominators as for any other building.
|
**An idle auto-recipe building still shows what it handles.** Having no selected recipe (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), it would otherwise show empty sections whenever it happens to be between cycles. Its input and output sections instead list the unlocked items of every recipe of its type — the same union its buffers were sized over — with a count and no per-cycle denominator, since no one recipe is in force. While a cycle is running, that cycle's recipe supplies the denominators as for any other building.
|
||||||
- REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon with its per-cycle amount, an arrow, each output item's icon with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production, and none while idle. A building with no recipe or schematic selected shows no summary.
|
- REQ-UI-RECIPE-SUMMARY: Below the recipe/schematic selection control, a building running a recipe or schematic shows a one-line **recipe summary**: each input item's icon with its per-cycle amount, an arrow, each output item's icon with its per-cycle amount, and the cycle time in seconds. It restates what the building will do without opening the selection dialog, and it is the panel's only display of the cycle time. For a Shipyard the summary is built from the schematic's materials and production time including the placed modules' contributions (REQ-BLD-SHIPYARD, REQ-MOD-STAT-CALC), matching the buffers beneath it. Auto-recipe buildings (Smelter, Reprocessing Plant — REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) have no player-selected recipe and so show no selection control; they show the summary of the recipe currently in production and, while between cycles, of the one they ran last. They keep it rather than dropping it, because a summary that came and went with each cycle would resize the card in step with the building's status (REQ-UI-SELECTION-STATUS), which is the one thing the panel must not do while the player is reading it (REQ-UI-SELECTION-PANEL). Such a building shows no summary only until it has run its first cycle. A building with no recipe or schematic selected shows no summary.
|
||||||
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section** between the input and output buffer sections (REQ-UI-SINGLE-SELECTION): a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all.
|
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the panel's runtime group shows a captioned **production section** between the input and output buffer sections (REQ-UI-SINGLE-SELECTION): a horizontal progress bar filled to the completion of the active production cycle, with that completion beside the caption as an integer percentage (e.g. `72%`), or the text `idle` in place of the percentage and an empty bar when no production cycle is active. The cycle time is shown in the recipe summary (REQ-UI-RECIPE-SUMMARY) rather than repeated here. When no recipe or schematic is selected, the production section is not shown at all.
|
||||||
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
- REQ-UI-MULTI-SELECT: The player selects multiple objects by box-drag or by Ctrl+clicking individual objects to add or remove them from the selection. Multi-select operates within a single category (REQ-UI-SELECTION-CATEGORIES). A box-drag that covers at least one building selects buildings (any field objects within the box are ignored — buildings win); a box-drag that covers no building but does cover ships, defence stations, or debris selects all of those field objects together (REQ-UI-ENTITY-CLICK-SELECT, REQ-UI-DEBRIS-MULTI-SELECT).
|
||||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress.
|
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected and the selection does not aggregate (REQ-UI-SELECTION-AGGREGATE), the panel shows a count summary. Its header names the size of the selection as `<n> buildings` in place of an object name, and carries no symbol and nothing in its right slot. Below it is one row per selected building type — the type's symbol, its name, and the number selected as `x<count>` — one type per row, and no per-building detail. A final row shows the **total building block cost** of the selection, captioned `Total cost` with the value followed by the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON): the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button bar); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-CARD). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
||||||
@@ -32,6 +33,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public:
|
|||||||
void forEach(Func&& f) const;
|
void forEach(Func&& f) const;
|
||||||
|
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
bool hasAll(entt::entity entity);
|
bool hasAll(entt::entity entity) const;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
T& get(entt::entity entity);
|
T& get(entt::entity entity);
|
||||||
@@ -101,7 +101,7 @@ void EntityAdmin::forEach(Func&& f) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Ts>
|
template <typename... Ts>
|
||||||
bool EntityAdmin::hasAll(entt::entity entity)
|
bool EntityAdmin::hasAll(entt::entity entity) const
|
||||||
{
|
{
|
||||||
return m_registry.all_of<Ts...>(entity);
|
return m_registry.all_of<Ts...>(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
76
src/lib/core/FloatingPanelPlacement.cpp
Normal file
76
src/lib/core/FloatingPanelPlacement.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
||||||
|
int leftPx, int rightPx, int marginPx)
|
||||||
|
{
|
||||||
|
int bottomPx = band.bottom();
|
||||||
|
for (const QRect& occupied : occupiedRects)
|
||||||
|
{
|
||||||
|
if (occupied.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Only what is actually in the way counts: a widget entirely to one side of this
|
||||||
|
// span is not below it, however tall it is.
|
||||||
|
if (occupied.right() < leftPx || occupied.left() > rightPx)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bottomPx = std::min(bottomPx, occupied.top() - marginPx - 1);
|
||||||
|
}
|
||||||
|
return bottomPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
|
||||||
|
int marginPx)
|
||||||
|
{
|
||||||
|
// What each side offers: the gap between the anchor and that edge of the band, less
|
||||||
|
// the margin the panel keeps from the anchor.
|
||||||
|
const int roomRightPx = band.right() - anchorRect.right() - marginPx;
|
||||||
|
const int roomLeftPx = anchorRect.left() - band.left() - marginPx;
|
||||||
|
|
||||||
|
if (roomRightPx >= widthPx)
|
||||||
|
{
|
||||||
|
return PanelSide::Right;
|
||||||
|
}
|
||||||
|
if (roomLeftPx >= widthPx)
|
||||||
|
{
|
||||||
|
return PanelSide::Left;
|
||||||
|
}
|
||||||
|
// Neither side can hold it without covering the selection, so it goes where it
|
||||||
|
// covers the least of it.
|
||||||
|
return (roomRightPx >= roomLeftPx) ? PanelSide::Right : PanelSide::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
||||||
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
int marginPx)
|
||||||
|
{
|
||||||
|
const int widthPx = std::min(wantedSize.width(), band.width());
|
||||||
|
|
||||||
|
// Against the anchor on the chosen side, growing away from it: the edge facing the
|
||||||
|
// selection is the one that stays put as the panel's content resizes.
|
||||||
|
int leftPx = (side == PanelSide::Right) ? anchorRect.right() + marginPx + 1
|
||||||
|
: anchorRect.left() - marginPx - widthPx;
|
||||||
|
// A panel that does not fit there is pushed back inside the view rather than hanging
|
||||||
|
// off it, which is what puts it over the selection when neither side had room.
|
||||||
|
leftPx = std::min(leftPx, band.right() - widthPx + 1);
|
||||||
|
leftPx = std::max(leftPx, band.left());
|
||||||
|
|
||||||
|
// Only the widgets its own column meets can shorten it.
|
||||||
|
const int bottomPx = getAvailableBottomPx(band, occupiedRects, leftPx,
|
||||||
|
leftPx + widthPx - 1, marginPx);
|
||||||
|
const int heightPx =
|
||||||
|
std::min(wantedSize.height(), std::max(0, bottomPx - band.top() + 1));
|
||||||
|
|
||||||
|
// Top-aligned with the anchor, then lifted by however much of it hangs below what is
|
||||||
|
// free. Never above the band: a panel taller than the space left is capped instead,
|
||||||
|
// and scrolls.
|
||||||
|
int topPx = std::max(anchorRect.top(), band.top());
|
||||||
|
topPx = std::min(topPx, bottomPx - heightPx + 1);
|
||||||
|
topPx = std::max(topPx, band.top());
|
||||||
|
|
||||||
|
return QRect(leftPx, topPx, widthPx, heightPx);
|
||||||
|
}
|
||||||
43
src/lib/core/FloatingPanelPlacement.h
Normal file
43
src/lib/core/FloatingPanelPlacement.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
#include <QSize>
|
||||||
|
|
||||||
|
// Geometry for the widgets floating over the game world view (REQ-UI-WORLD-SIZE). Their
|
||||||
|
// owner places them in one ordered pass, each into the space the earlier ones left free,
|
||||||
|
// and these are the rules they place themselves by. Pure geometry -- no widget is
|
||||||
|
// involved, which is what lets the rules be tested without a display.
|
||||||
|
|
||||||
|
// The lowest bottom edge available to a widget occupying the horizontal span
|
||||||
|
// [leftPx, rightPx] inside band: the band's own bottom, or marginPx above the topmost
|
||||||
|
// occupied rectangle whose horizontal extent meets that span. A rectangle beside the
|
||||||
|
// span is not in the way and does not shorten it (REQ-UI-SELECTION-PANEL,
|
||||||
|
// REQ-UI-CONTROLS-PANEL). The result is inclusive, as QRect::bottom() is.
|
||||||
|
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
||||||
|
int leftPx, int rightPx, int marginPx);
|
||||||
|
|
||||||
|
// Which side of the selection the panel stands on (REQ-UI-SELECTION-PANEL).
|
||||||
|
enum class PanelSide
|
||||||
|
{
|
||||||
|
Right,
|
||||||
|
Left
|
||||||
|
};
|
||||||
|
|
||||||
|
// The side a panel widthPx wide takes beside anchorRect: the right of it where it fits
|
||||||
|
// within band, otherwise the left, and where it fits on neither, whichever side leaves
|
||||||
|
// more room -- the one case in which the panel ends up over the selection
|
||||||
|
// (REQ-UI-SELECTION-PANEL). Decided once when the selection starts and kept for as long
|
||||||
|
// as it lasts, so a card that grows later never flips the panel across the object.
|
||||||
|
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
|
||||||
|
int marginPx);
|
||||||
|
|
||||||
|
// Where a panel of wantedSize stands beside anchorRect on the given side: separated from
|
||||||
|
// it by marginPx and growing away from it, its top edge on the anchor's top edge, pushed
|
||||||
|
// inside band and above whatever occupies it. The returned height is short of
|
||||||
|
// wantedSize's when there was not enough room, which is the caller's cue to scroll its
|
||||||
|
// content (REQ-UI-SELECTION-PANEL).
|
||||||
|
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
||||||
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
int marginPx);
|
||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionAnchorChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
||||||
@@ -43,6 +44,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingLayoutInvalidatedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
12
src/lib/eventsystem/event/FloatingLayoutInvalidatedEvent.h
Normal file
12
src/lib/eventsystem/event/FloatingLayoutInvalidatedEvent.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Asks the owner of the widgets floating over the game world view to re-run its placement
|
||||||
|
// pass (see ui/FloatingPanel.h). Published by a floating widget whose content or
|
||||||
|
// visibility changed: what space that widget may take depends on the ones placed before
|
||||||
|
// it, so it cannot re-place itself alone (REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL,
|
||||||
|
// REQ-UI-SELECTION-PANEL). Carries no payload -- the pass re-reads every widget.
|
||||||
|
class FloatingLayoutInvalidatedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
25
src/lib/eventsystem/event/SelectionAnchorChangedEvent.h
Normal file
25
src/lib/eventsystem/event/SelectionAnchorChangedEvent.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Where on the screen the selection that is about to be made sits: the bounds of the one
|
||||||
|
// object selected, or of all of them when the selection starts as a multi-selection
|
||||||
|
// (REQ-UI-SELECTION-PANEL). The rectangle is in the game world view's own widget
|
||||||
|
// coordinates.
|
||||||
|
//
|
||||||
|
// Published only when a selection *starts* -- a plain click or drag, or an additive one
|
||||||
|
// onto an empty selection -- and always immediately before the selection itself. Adding
|
||||||
|
// to a selection publishes nothing, which is what leaves the selection panel where it
|
||||||
|
// is while the selection grows; and because the rectangle is screen space frozen at that
|
||||||
|
// moment, scrolling the view or a selected ship flying off does not move the panel
|
||||||
|
// either.
|
||||||
|
class SelectionAnchorChangedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SelectionAnchorChangedEvent(QRect rectPx)
|
||||||
|
: rectPx(rectPx) {}
|
||||||
|
|
||||||
|
const QRect rectPx;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ add_files(
|
|||||||
TunnelCompletionTest.cpp
|
TunnelCompletionTest.cpp
|
||||||
WorldCoordinatesTest.cpp
|
WorldCoordinatesTest.cpp
|
||||||
WorldCameraTest.cpp
|
WorldCameraTest.cpp
|
||||||
|
FloatingPanelPlacementTest.cpp
|
||||||
SelectionControllerTest.cpp
|
SelectionControllerTest.cpp
|
||||||
BuildModeControllerTest.cpp
|
BuildModeControllerTest.cpp
|
||||||
ControlActionTest.cpp
|
ControlActionTest.cpp
|
||||||
|
|||||||
156
src/test/FloatingPanelPlacementTest.cpp
Normal file
156
src/test/FloatingPanelPlacementTest.cpp
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
|
|
||||||
|
// The band every case below places into: 1000x600, so a bottom edge of 599.
|
||||||
|
static QRect makeBand()
|
||||||
|
{
|
||||||
|
return QRect(0, 0, 1000, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const int kMarginPx = 8;
|
||||||
|
|
||||||
|
TEST_CASE("With nothing in the way a widget may use the whole band", "[layout]")
|
||||||
|
{
|
||||||
|
// REQ-UI-SELECTION-PANEL: the band's own bottom is the limit when no other floating
|
||||||
|
// widget has been placed yet.
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), {}, 0, 999, kMarginPx) == 599);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A widget in the same column pushes the bottom above it", "[layout]")
|
||||||
|
{
|
||||||
|
// The build button bar sitting at the bottom center leaves the space above it, less
|
||||||
|
// the margin kept between the two (REQ-UI-BUILD-BAR).
|
||||||
|
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 350, 650, kMarginPx) == 511);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A widget beside the column does not shorten it", "[layout]")
|
||||||
|
{
|
||||||
|
// The controls panel in the bottom-left corner is not in the way of a panel standing
|
||||||
|
// at the right edge, however tall it is (REQ-UI-CONTROLS-PANEL).
|
||||||
|
const std::vector<QRect> occupied = { QRect(0, 100, 200, 499) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 700, 999, kMarginPx) == 599);
|
||||||
|
|
||||||
|
// Touching columns do count as meeting: the panel starts exactly where the widget
|
||||||
|
// ends, which is an overlap of one pixel.
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 199, 999, kMarginPx) == 91);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The topmost widget in the column decides", "[layout]")
|
||||||
|
{
|
||||||
|
// Several widgets meet the column: the one that reaches highest is the binding one,
|
||||||
|
// whatever order they are given in.
|
||||||
|
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72),
|
||||||
|
QRect(0, 300, 500, 299),
|
||||||
|
QRect(900, 560, 100, 40) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 450, 550, kMarginPx) == 291);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An empty rectangle occupies nothing", "[layout]")
|
||||||
|
{
|
||||||
|
// A floating widget that is hidden contributes a null rect rather than being left
|
||||||
|
// out of the pass.
|
||||||
|
const std::vector<QRect> occupied = { QRect(), QRect(400, 520, 200, 0) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 0, 999, kMarginPx) == 599);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A widget filling the column leaves nothing", "[layout]")
|
||||||
|
{
|
||||||
|
// The caller is expected to notice that the space left is not positive rather than
|
||||||
|
// being handed a floor of its own.
|
||||||
|
const std::vector<QRect> occupied = { QRect(0, 0, 1000, 600) };
|
||||||
|
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 0, 999, kMarginPx) == -9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Which side of the selection the panel takes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("The panel stands to the right of the selection where it fits", "[layout]")
|
||||||
|
{
|
||||||
|
// REQ-UI-SELECTION-PANEL: right of the anchor is the first choice.
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(100, 100, 60, 60), 300, kMarginPx)
|
||||||
|
== PanelSide::Right);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The panel goes left when the right cannot hold it", "[layout]")
|
||||||
|
{
|
||||||
|
// A selection near the right edge leaves 100 px there, not enough for a 300 px
|
||||||
|
// panel, and the left is wide open.
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(880, 100, 20, 60), 300, kMarginPx)
|
||||||
|
== PanelSide::Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Fitting on neither side, the panel takes the roomier one", "[layout]")
|
||||||
|
{
|
||||||
|
// A bounding box spanning most of the view: 192 px free on the left, 92 on the
|
||||||
|
// right, and a 300 px panel fits in neither. It covers as little as it can.
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(200, 100, 700, 200), 300, kMarginPx)
|
||||||
|
== PanelSide::Left);
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(100, 100, 700, 200), 300, kMarginPx)
|
||||||
|
== PanelSide::Right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Where it then stands
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("The panel sits beside the anchor with its top edges aligned", "[layout]")
|
||||||
|
{
|
||||||
|
// REQ-UI-SELECTION-PANEL: separated by the margin, growing away from the selection,
|
||||||
|
// top edge on the anchor's top edge.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 120, 60, 60),
|
||||||
|
PanelSide::Right, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 120, 300, 200));
|
||||||
|
|
||||||
|
const QRect placedLeft = placeBesideAnchor(makeBand(), QRect(500, 120, 60, 60),
|
||||||
|
PanelSide::Left, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placedLeft == QRect(192, 120, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel that would hang below the view is lifted", "[layout]")
|
||||||
|
{
|
||||||
|
// Top-aligning with a selection low in the view would put the panel's bottom past
|
||||||
|
// the band, so it rises until it fits rather than overrunning it.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 500, 60, 60),
|
||||||
|
PanelSide::Right, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 400, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel standing over another widget rises above it", "[layout]")
|
||||||
|
{
|
||||||
|
// The controls panel in the bottom-left is in the way of a panel placed to the left
|
||||||
|
// of a selection: it clears the top of it by the margin (REQ-UI-CONTROLS-PANEL).
|
||||||
|
const std::vector<QRect> occupied = { QRect(0, 300, 260, 300) };
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(500, 250, 60, 60),
|
||||||
|
PanelSide::Left, QSize(300, 200), occupied,
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(192, 92, 300, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel taller than the space left is capped", "[layout]")
|
||||||
|
{
|
||||||
|
// Capping is the caller's cue to scroll: it asked for 700 and got what there was.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 100, 60, 60),
|
||||||
|
PanelSide::Right, QSize(300, 700), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 0, 300, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A panel that fits on neither side is pushed inside the view", "[layout]")
|
||||||
|
{
|
||||||
|
// The one case where it covers part of the selection (REQ-UI-SELECTION-PANEL): it
|
||||||
|
// stands as far from the anchor as the band allows, not off the edge of it.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 100, 700, 200),
|
||||||
|
PanelSide::Right, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(700, 100, 300, 200));
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "DisplayName.h"
|
#include "DisplayName.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "ExitBuilderModeRequestedEvent.h"
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
#include "IconCaption.h"
|
#include "IconCaption.h"
|
||||||
#include "InputMapper.h"
|
#include "InputMapper.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
@@ -270,15 +271,25 @@ BuildButtonBar::~BuildButtonBar()
|
|||||||
unregisterForEvents();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::anchorTo(const QRect& worldViewRect)
|
void BuildButtonBar::placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& /*occupiedRects*/)
|
||||||
{
|
{
|
||||||
m_viewRect = worldViewRect;
|
if (viewRect.isNull())
|
||||||
recenter();
|
{
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
// The layout drops hidden buttons from its size hint, but only once it has been
|
||||||
|
// re-run: an unlock changes which buttons are shown, and Qt would not get around to
|
||||||
|
// it before the bar is measured here.
|
||||||
|
layout()->activate();
|
||||||
|
|
||||||
int BuildButtonBar::getStripHeightPx() const
|
const QSize barSize = sizeHint();
|
||||||
{
|
// Centered, except that a bar wider than the view stays flush with its left edge
|
||||||
return height() + kBottomMarginPx;
|
// rather than hanging off both sides.
|
||||||
|
const int x = qMax(viewRect.left(),
|
||||||
|
viewRect.left() + (viewRect.width() - barSize.width()) / 2);
|
||||||
|
const int y = viewRect.bottom() - kBottomMarginPx - barSize.height() + 1;
|
||||||
|
setGeometry(QRect(QPoint(x, y), barSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::clearActiveButton()
|
void BuildButtonBar::clearActiveButton()
|
||||||
@@ -328,29 +339,11 @@ void BuildButtonBar::updateVisibility()
|
|||||||
{
|
{
|
||||||
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
|
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
|
||||||
}
|
}
|
||||||
// A hidden button leaves the row, so the bar has to take up its new width and
|
// A hidden button leaves the row, so the bar takes up a new width and has to be
|
||||||
// re-center on it (REQ-UI-BUILD-BAR).
|
// re-centered on it -- and the widgets that keep clear of the bar have to be placed
|
||||||
recenter();
|
// against that new rect too, so the whole pass is re-run (REQ-UI-BUILD-BAR).
|
||||||
}
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
void BuildButtonBar::recenter()
|
|
||||||
{
|
|
||||||
if (m_viewRect.isNull())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The layout drops hidden buttons from its size hint, but only once it has been
|
|
||||||
// re-run: updateVisibility() calls this straight after setVisible(), before Qt
|
|
||||||
// would get around to it on its own.
|
|
||||||
layout()->activate();
|
|
||||||
|
|
||||||
const QSize barSize = sizeHint();
|
|
||||||
// Centered, except that a bar wider than the view stays flush with its left edge
|
|
||||||
// rather than hanging off both sides.
|
|
||||||
const int x = qMax(m_viewRect.left(),
|
|
||||||
m_viewRect.left() + (m_viewRect.width() - barSize.width()) / 2);
|
|
||||||
const int y = m_viewRect.bottom() - kBottomMarginPx - barSize.height() + 1;
|
|
||||||
setGeometry(QRect(QPoint(x, y), barSize));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::onBuildButton(int index)
|
void BuildButtonBar::onBuildButton(int index)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "DeconstructModeChangedEvent.h"
|
#include "DeconstructModeChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "UnlockedBuildingsChangedEvent.h"
|
#include "UnlockedBuildingsChangedEvent.h"
|
||||||
|
|
||||||
@@ -24,10 +25,12 @@ class BuildingIconCache;
|
|||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
|
|
||||||
// The build menu: one horizontal row of build buttons floating over the game world
|
// The build menu: one horizontal row of build buttons floating over the game world
|
||||||
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons; its owner hands it the
|
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons and centers itself along the
|
||||||
// world view's rect through anchorTo() and it centers itself along that rect's
|
// bottom edge of the rect its owner places it in. It is placed first of the floating
|
||||||
// bottom edge.
|
// widgets and so takes the space it wants outright: nothing ever moves it aside, and the
|
||||||
|
// widgets placed after it keep out of its way instead.
|
||||||
class BuildButtonBar : public QWidget,
|
class BuildButtonBar : public QWidget,
|
||||||
|
public FloatingPanel,
|
||||||
public CombinedEventHandler<BuilderModeExitedEvent,
|
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
DeconstructModeChangedEvent,
|
DeconstructModeChangedEvent,
|
||||||
BuildHotkeyPressedEvent,
|
BuildHotkeyPressedEvent,
|
||||||
@@ -45,15 +48,11 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~BuildButtonBar() override;
|
~BuildButtonBar() override;
|
||||||
|
|
||||||
// Centers the bar along the bottom edge of the game world view's rect, given in
|
// Centers the bar along the bottom edge of the game world view's rect, given in the
|
||||||
// the bar's parent coordinates (REQ-UI-BUILD-BAR). The rect is remembered, so a
|
// bar's parent coordinates (REQ-UI-BUILD-BAR). Nothing is occupied yet when the bar
|
||||||
// re-center later driven by an unlock needs no second call from the owner.
|
// is placed, so it ignores what it is handed there.
|
||||||
void anchorTo(const QRect& worldViewRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
// Height of the strip the bar occupies along the bottom of the world view: its own
|
|
||||||
// height plus the margin below it. The selection panel keeps out of this strip, and
|
|
||||||
// the bar never moves for the panel in return (REQ-UI-BUILD-BAR).
|
|
||||||
int getStripHeightPx() const;
|
|
||||||
|
|
||||||
void clearActiveButton();
|
void clearActiveButton();
|
||||||
|
|
||||||
@@ -67,11 +66,6 @@ private:
|
|||||||
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
|
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
|
||||||
void updateVisibility();
|
void updateVisibility();
|
||||||
|
|
||||||
// Shrinks the bar to its currently shown buttons and re-centers it in the
|
|
||||||
// anchored rect (REQ-UI-BUILD-BAR). Does nothing until anchorTo() supplied that
|
|
||||||
// rect, so the construction-time call is harmless.
|
|
||||||
void recenter();
|
|
||||||
|
|
||||||
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
||||||
@@ -91,5 +85,4 @@ private:
|
|||||||
std::map<BuildingType, int> m_costs;
|
std::map<BuildingType, int> m_costs;
|
||||||
std::optional<std::size_t> m_activeIndex;
|
std::optional<std::size_t> m_activeIndex;
|
||||||
QPushButton* m_deconstructButton;
|
QPushButton* m_deconstructButton;
|
||||||
QRect m_viewRect;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||||
@@ -47,6 +49,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "ControlActionText.h"
|
#include "ControlActionText.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
#include "selection/SelectionNames.h"
|
#include "selection/SelectionNames.h"
|
||||||
|
|
||||||
@@ -159,11 +162,10 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::anchorTo(const QRect& worldRect, const QRect& buildBarRect)
|
void ControlsPanel::invalidateLayout()
|
||||||
{
|
{
|
||||||
m_worldRect = worldRect;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
m_buildBarRect = buildBarRect;
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
refit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
||||||
@@ -174,7 +176,7 @@ void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
|||||||
{
|
{
|
||||||
m_collapsed = !m_collapsed;
|
m_collapsed = !m_collapsed;
|
||||||
m_scrollArea->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
@@ -241,7 +243,7 @@ void ControlsPanel::rebuild(const ControlContext& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_scrollArea->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
||||||
@@ -277,9 +279,9 @@ QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
|||||||
return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
|
return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::refit()
|
void ControlsPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
|
||||||
{
|
{
|
||||||
if (m_worldRect.isNull()) { return; }
|
if (viewRect.isNull()) { return; }
|
||||||
|
|
||||||
// Rows are torn down and rebuilt wholesale, and a widget added to a layout is only
|
// Rows are torn down and rebuilt wholesale, and a widget added to a layout is only
|
||||||
// shown once that layout runs -- without this the new rows count for nothing and
|
// shown once that layout runs -- without this the new rows count for nothing and
|
||||||
@@ -290,7 +292,7 @@ void ControlsPanel::refit()
|
|||||||
m_rowsLayout->invalidate();
|
m_rowsLayout->invalidate();
|
||||||
m_rowsLayout->activate();
|
m_rowsLayout->activate();
|
||||||
|
|
||||||
const QRect band = m_worldRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
if (band.width() <= 0 || band.height() <= 0) { return; }
|
if (band.width() <= 0 || band.height() <= 0) { return; }
|
||||||
|
|
||||||
// Measured from the heading and the rows directly rather than from the panel's own
|
// Measured from the heading and the rows directly rather than from the panel's own
|
||||||
@@ -315,20 +317,16 @@ void ControlsPanel::refit()
|
|||||||
// The bottom-left corner of the view, growing upward as rows are added
|
// The bottom-left corner of the view, growing upward as rows are added
|
||||||
// (REQ-UI-CONTROLS-PANEL).
|
// (REQ-UI-CONTROLS-PANEL).
|
||||||
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
||||||
int heightPx = qMin(wantedHeightPx, band.height());
|
|
||||||
int topPx = band.bottom() - heightPx + 1;
|
|
||||||
|
|
||||||
// The build button bar is centered and sized to its buttons, so it usually leaves
|
// The build button bar is centered and sized to its buttons, so it usually leaves
|
||||||
// this corner free and the panel can share the bottom edge with it. Only when the
|
// this corner free and the panel can share the bottom edge with it. Only where the
|
||||||
// two would actually overlap does the panel rise, clearing the bar's top by the
|
// two would actually meet does the panel rise, clearing the bar's top by the same
|
||||||
// same margin it keeps from the view's edges (REQ-UI-BUILD-BAR).
|
// margin it keeps from the view's edges (REQ-UI-BUILD-BAR). The bar is the only
|
||||||
const QRect wanted(band.left(), topPx, widthPx, heightPx);
|
// widget placed before this one, so it is the only rect that can be in the way.
|
||||||
if (!m_buildBarRect.isNull() && wanted.intersects(m_buildBarRect))
|
const int bottomPx = getAvailableBottomPx(band, occupiedRects, band.left(),
|
||||||
{
|
band.left() + widthPx - 1, kMarginPx);
|
||||||
const int availablePx = m_buildBarRect.top() - kMarginPx - band.top();
|
int heightPx = qMin(wantedHeightPx, qMax(0, bottomPx - band.top() + 1));
|
||||||
heightPx = qMin(heightPx, qMax(0, availablePx));
|
int topPx = bottomPx - heightPx + 1;
|
||||||
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
|
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
|
||||||
// own width, or it would appear over the labels.
|
// own width, or it would appear over the labels.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "ControlAction.h"
|
#include "ControlAction.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
|
|
||||||
class GameWorldView;
|
class GameWorldView;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
@@ -29,7 +30,7 @@ class QVBoxLayout;
|
|||||||
// while the game is paused, so there is no tick to hang it on either. The rebuild is
|
// while the game is paused, so there is no tick to hang it on either. The rebuild is
|
||||||
// skipped unless the resolved content actually differs, which is a vector of enums to
|
// skipped unless the resolved content actually differs, which is a vector of enums to
|
||||||
// compare.
|
// compare.
|
||||||
class ControlsPanel : public QWidget
|
class ControlsPanel : public QWidget, public FloatingPanel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -42,10 +43,11 @@ public:
|
|||||||
// Places the panel in the bottom-left corner of the game world view. It shares the
|
// Places the panel in the bottom-left corner of the game world view. It shares the
|
||||||
// bottom edge with the build button bar rather than clearing its strip, the bar
|
// bottom edge with the build button bar rather than clearing its strip, the bar
|
||||||
// being centered and sized to its buttons and so usually leaving the left free; it
|
// being centered and sized to its buttons and so usually leaving the left free; it
|
||||||
// rises above the bar only when the two would otherwise overlap. `buildBarRect` is
|
// rises above the bar only when the two would otherwise overlap
|
||||||
// the bar's current geometry in the same coordinates, or a null rect when there is
|
// (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR). The bar is the only thing placed before
|
||||||
// none to avoid (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR).
|
// it, so it is the only rect it ever has to rise above.
|
||||||
void anchorTo(const QRect& worldRect, const QRect& buildBarRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Clicking the heading collapses and expands the panel (REQ-UI-CONTROLS-PANEL).
|
// Clicking the heading collapses and expands the panel (REQ-UI-CONTROLS-PANEL).
|
||||||
@@ -59,8 +61,8 @@ private:
|
|||||||
// The heading's "<name> * <detail>" text for the context, detail omitted when the
|
// The heading's "<name> * <detail>" text for the context, detail omitted when the
|
||||||
// context has none.
|
// context has none.
|
||||||
QString getHeadingText(const ControlContext& context) const;
|
QString getHeadingText(const ControlContext& context) const;
|
||||||
// Re-fits the panel to its content within the anchored band.
|
// Asks for the placement pass to be re-run, the panel's own size having changed.
|
||||||
void refit();
|
void invalidateLayout();
|
||||||
|
|
||||||
const GameWorldView* m_view;
|
const GameWorldView* m_view;
|
||||||
|
|
||||||
@@ -82,9 +84,4 @@ private:
|
|||||||
// Survives context changes and simulation restarts; presentation only, never a
|
// Survives context changes and simulation restarts; presentation only, never a
|
||||||
// command (REQ-UI-CONTROLS-PANEL).
|
// command (REQ-UI-CONTROLS-PANEL).
|
||||||
bool m_collapsed = false;
|
bool m_collapsed = false;
|
||||||
|
|
||||||
// The world view the panel sits in, and the build button bar it steps around, both
|
|
||||||
// in the coordinates of its parent. Null until the owner has anchored it.
|
|
||||||
QRect m_worldRect;
|
|
||||||
QRect m_buildBarRect;
|
|
||||||
};
|
};
|
||||||
|
|||||||
27
src/ui/FloatingPanel.h
Normal file
27
src/ui/FloatingPanel.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
// A widget floating over the game world view (REQ-UI-WORLD-SIZE). MainWindow places all
|
||||||
|
// of them in one ordered pass -- build button bar, then controls panel, then selection
|
||||||
|
// panel -- and each places itself into the space the earlier ones have not taken. The
|
||||||
|
// order is the priority the requirements state: the bar never moves for anyone
|
||||||
|
// (REQ-UI-BUILD-BAR), the controls panel steps around the bar (REQ-UI-CONTROLS-PANEL),
|
||||||
|
// and keeping clear of both is the selection panel's job (REQ-UI-SELECTION-PANEL).
|
||||||
|
//
|
||||||
|
// A widget never re-places itself, because what it may take depends on the widgets placed
|
||||||
|
// before it. It instead publishes FloatingLayoutInvalidatedEvent whenever its content or
|
||||||
|
// its visibility changed, and the owner re-runs the whole pass.
|
||||||
|
class FloatingPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FloatingPanel() = default;
|
||||||
|
|
||||||
|
// Sets this widget's own geometry within viewRect, keeping clear of occupiedRects --
|
||||||
|
// the geometry of every floating widget already placed in this pass, in the same
|
||||||
|
// coordinates. A widget with nothing to show hides itself and takes no space.
|
||||||
|
virtual void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) = 0;
|
||||||
|
};
|
||||||
@@ -47,6 +47,8 @@
|
|||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "DebrisSystem.h"
|
#include "DebrisSystem.h"
|
||||||
|
#include "SelectionAnchorChangedEvent.h"
|
||||||
|
#include "SelectionBounds.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -1255,6 +1257,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
|
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
|
||||||
if (buildingHit.has_value())
|
if (buildingHit.has_value())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {*buildingHit}, {}, {});
|
||||||
m_selection.selectBuildings({*buildingHit}, mode);
|
m_selection.selectBuildings({*buildingHit}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1262,6 +1265,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||||
if (actorHit != entt::null)
|
if (actorHit != entt::null)
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, {actorHit}, {});
|
||||||
m_selection.selectFieldObjects({actorHit}, {}, mode);
|
m_selection.selectFieldObjects({actorHit}, {}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1269,6 +1273,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
const entt::entity debrisHit = debrisAtWorldPos(m_sim->getAdmin(), worldPos);
|
const entt::entity debrisHit = debrisAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||||
if (debrisHit != entt::null)
|
if (debrisHit != entt::null)
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, {}, {debrisHit});
|
||||||
m_selection.selectFieldObjects({}, {debrisHit}, mode);
|
m_selection.selectFieldObjects({}, {debrisHit}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1289,6 +1294,7 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
||||||
if (!boxIds.empty())
|
if (!boxIds.empty())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, boxIds, {}, {});
|
||||||
m_selection.selectBuildings(boxIds, mode);
|
m_selection.selectBuildings(boxIds, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1305,7 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
||||||
if (!boxActors.empty() || !boxDebris.empty())
|
if (!boxActors.empty() || !boxDebris.empty())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, boxActors, boxDebris);
|
||||||
m_selection.selectFieldObjects(boxActors, boxDebris, mode);
|
m_selection.selectFieldObjects(boxActors, boxDebris, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1307,6 +1314,35 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
if (!additive) { m_selection.clearAll(); }
|
if (!additive) { m_selection.clearAll(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::publishSelectionAnchor(SelectionMode mode,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
// An additive gesture onto something already selected is growing that selection, not
|
||||||
|
// starting one, and the panel stays where it was put (REQ-UI-SELECTION-PANEL).
|
||||||
|
const bool startsSelection =
|
||||||
|
(mode == SelectionMode::Replace) || (m_selection.getSelectedBuildings().empty()
|
||||||
|
&& m_selection.getSelectedActors().empty()
|
||||||
|
&& m_selection.getSelectedDebris().empty());
|
||||||
|
if (!startsSelection)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen space, frozen here: the panel is placed against where the selection is at
|
||||||
|
// this moment and stays there, however far the view scrolls or the objects move
|
||||||
|
// afterwards.
|
||||||
|
const QRect anchorRect = getSelectionWidgetRect(*m_sim, getCoordinates(),
|
||||||
|
buildings, actors, debris);
|
||||||
|
if (anchorRect.isNull())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionAnchorChangedEvent>(anchorRect));
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||||
{
|
{
|
||||||
const WorldCoordinates coordinates = getCoordinates();
|
const WorldCoordinates coordinates = getCoordinates();
|
||||||
|
|||||||
@@ -226,6 +226,15 @@ private:
|
|||||||
// which is the only case that goes on to start a box drag.
|
// which is the only case that goes on to start a box drag.
|
||||||
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
||||||
void selectInBox(bool additive);
|
void selectInBox(bool additive);
|
||||||
|
// Publishes where on the screen the selection about to be made sits, so the
|
||||||
|
// selection panel can be placed beside it (REQ-UI-SELECTION-PANEL). Called with
|
||||||
|
// what is about to be selected, immediately before selecting it, and publishes
|
||||||
|
// nothing unless that selection is starting rather than growing.
|
||||||
|
void publishSelectionAnchor(
|
||||||
|
SelectionMode mode,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris);
|
||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
|
|||||||
@@ -172,19 +172,50 @@ void MainWindow::layoutPanels()
|
|||||||
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
||||||
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
||||||
m_gameWorldView->setGeometry(worldRect);
|
m_gameWorldView->setGeometry(worldRect);
|
||||||
// Sizes itself to its buttons and centers along the bottom of the world view
|
|
||||||
// (REQ-UI-BUILD-BAR).
|
|
||||||
m_buildButtonBar->anchorTo(worldRect);
|
|
||||||
// The panel confines itself to what the bar leaves free, so the bar never has to
|
|
||||||
// move for it (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
|
||||||
m_selectionPanel->anchorTo(
|
|
||||||
worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
|
|
||||||
// The opposite corner: bottom-left of the view, sharing the bottom edge with the
|
|
||||||
// bar rather than clearing its whole strip, and rising only if the two would meet.
|
|
||||||
// Anchored after the bar so the geometry it steps around is the current one
|
|
||||||
// (REQ-UI-CONTROLS-PANEL).
|
|
||||||
m_controlsPanel->anchorTo(worldRect, m_buildButtonBar->geometry());
|
|
||||||
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
||||||
|
|
||||||
|
// The floating widgets are placed in one ordered pass, each into the space the
|
||||||
|
// earlier ones have not taken (FloatingPanel.h). The order is the priority the
|
||||||
|
// requirements state: the build button bar takes what it wants and never moves for
|
||||||
|
// anyone (REQ-UI-BUILD-BAR), the controls panel steps around the bar
|
||||||
|
// (REQ-UI-CONTROLS-PANEL), and the selection panel keeps clear of both
|
||||||
|
// (REQ-UI-SELECTION-PANEL). A widget with nothing to show hides itself in placeIn()
|
||||||
|
// and takes no space.
|
||||||
|
//
|
||||||
|
// Re-entry is refused rather than queued: setGeometry() on a widget in the pass can
|
||||||
|
// reach code that asks for another pass, and the one already running is about to
|
||||||
|
// produce the same answer.
|
||||||
|
if (m_layingOut)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_layingOut = true;
|
||||||
|
|
||||||
|
const std::vector<QWidget*> floatingWidgets = {
|
||||||
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
||||||
|
const std::vector<FloatingPanel*> floatingPanels = {
|
||||||
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
||||||
|
|
||||||
|
std::vector<QRect> occupiedRects;
|
||||||
|
for (std::size_t i = 0; i < floatingPanels.size(); ++i)
|
||||||
|
{
|
||||||
|
floatingPanels[i]->placeIn(worldRect, occupiedRects);
|
||||||
|
if (floatingWidgets[i]->isVisible())
|
||||||
|
{
|
||||||
|
occupiedRects.push_back(floatingWidgets[i]->geometry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_layingOut = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleEvent(
|
||||||
|
std::shared_ptr<const FloatingLayoutInvalidatedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
// One of the floating widgets changed size or visibility. What each of them may take
|
||||||
|
// depends on the ones placed before it, so the answer is the whole pass rather than
|
||||||
|
// that one widget re-placing itself.
|
||||||
|
layoutPanels();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "EscapeMenuRequestedEvent.h"
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "GameOverEvent.h"
|
#include "GameOverEvent.h"
|
||||||
#include "LayoutDialogRequestedEvent.h"
|
#include "LayoutDialogRequestedEvent.h"
|
||||||
@@ -45,7 +46,8 @@ class MainWindow : public QWidget,
|
|||||||
LayoutDialogRequestedEvent,
|
LayoutDialogRequestedEvent,
|
||||||
RecipeSelectionRequestedEvent,
|
RecipeSelectionRequestedEvent,
|
||||||
BlueprintSaveRequestedEvent,
|
BlueprintSaveRequestedEvent,
|
||||||
BlueprintSelectionRequestedEvent>
|
BlueprintSelectionRequestedEvent,
|
||||||
|
FloatingLayoutInvalidatedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -67,6 +69,7 @@ private:
|
|||||||
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 BlueprintSaveRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const FloatingLayoutInvalidatedEvent> 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
|
||||||
@@ -85,6 +88,8 @@ private:
|
|||||||
// both callers already hold theirs, which is what keeps the dim continuous when a
|
// 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).
|
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
||||||
void showBlueprintSelectionDialog();
|
void showBlueprintSelectionDialog();
|
||||||
|
// Places the widgets floating over the game world view, in one ordered pass
|
||||||
|
// (FloatingPanel.h). Runs on a resize and on every FloatingLayoutInvalidatedEvent.
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -109,4 +114,8 @@ private:
|
|||||||
|
|
||||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||||
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
||||||
|
|
||||||
|
// Set while the placement pass runs, so a widget placed in it cannot start a second
|
||||||
|
// pass from inside the first.
|
||||||
|
bool m_layingOut = false;
|
||||||
};
|
};
|
||||||
|
|||||||
115
src/ui/SelectionBounds.cpp
Normal file
115
src/ui/SelectionBounds.cpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "SelectionBounds.h"
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "DebrisComponent.h"
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "PositionComponent.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
|
#include "WorldCoordinates.h"
|
||||||
|
#include "WorldPrimitives.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// The widget rectangle of a footprint anchored at a tile, both kinds of body being
|
||||||
|
// described the same way.
|
||||||
|
QRectF getFootprintWidgetRect(const WorldCoordinates& coordinates, QPoint anchor,
|
||||||
|
QSize footprint)
|
||||||
|
{
|
||||||
|
const QPointF topLeft = coordinates.tileToWidget(anchor);
|
||||||
|
return QRectF(topLeft.x(), topLeft.y(),
|
||||||
|
footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
|
||||||
|
footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
BuildingId id)
|
||||||
|
{
|
||||||
|
if (const Building* building = findBuilding(state, id))
|
||||||
|
{
|
||||||
|
return getFootprintWidgetRect(coordinates, building->anchor, building->footprint);
|
||||||
|
}
|
||||||
|
if (const ConstructionSite* site = findSite(state, id))
|
||||||
|
{
|
||||||
|
return getFootprintWidgetRect(coordinates, site->anchor, site->footprint);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QRectF> getActorWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity actor)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(actor))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (admin.hasAll<StationBodyComponent>(actor))
|
||||||
|
{
|
||||||
|
const StationBodyComponent& body = admin.get<StationBodyComponent>(actor);
|
||||||
|
return getFootprintWidgetRect(coordinates, body.anchor, body.footprint);
|
||||||
|
}
|
||||||
|
if (admin.hasAll<PositionComponent>(actor))
|
||||||
|
{
|
||||||
|
// A ship is a triangle about its center; the square its longest extent fits in
|
||||||
|
// is what the panel is placed beside.
|
||||||
|
const QPointF center =
|
||||||
|
coordinates.worldToWidget(admin.get<PositionComponent>(actor).value);
|
||||||
|
const qreal extent = static_cast<qreal>(getShipForwardExtentPx(coordinates));
|
||||||
|
return QRectF(center.x() - extent, center.y() - extent,
|
||||||
|
2.0 * extent, 2.0 * extent);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QRectF> getDebrisWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity debris)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(debris) || !admin.hasAll<PositionComponent, DebrisComponent>(debris))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const QPointF center =
|
||||||
|
coordinates.worldToWidget(admin.get<PositionComponent>(debris).value);
|
||||||
|
const qreal radius = static_cast<qreal>(getDebrisRadiusPx(coordinates));
|
||||||
|
return QRectF(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect getSelectionWidgetRect(const Simulation& sim, const WorldCoordinates& coordinates,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
QRectF bounds;
|
||||||
|
// A null rect unites to nothing of its own, so the first object found sets the box
|
||||||
|
// and every later one grows it.
|
||||||
|
auto add = [&bounds](const std::optional<QRectF>& rect)
|
||||||
|
{
|
||||||
|
if (rect.has_value())
|
||||||
|
{
|
||||||
|
bounds = bounds.isNull() ? *rect : bounds.united(*rect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (BuildingId id : buildings)
|
||||||
|
{
|
||||||
|
add(getBuildingWidgetRect(sim.getFactoryState(), coordinates, id));
|
||||||
|
}
|
||||||
|
for (entt::entity actor : actors)
|
||||||
|
{
|
||||||
|
add(getActorWidgetRect(sim.getAdmin(), coordinates, actor));
|
||||||
|
}
|
||||||
|
for (entt::entity piece : debris)
|
||||||
|
{
|
||||||
|
add(getDebrisWidgetRect(sim.getAdmin(), coordinates, piece));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds.isNull() ? QRect() : bounds.toAlignedRect();
|
||||||
|
}
|
||||||
48
src/ui/SelectionBounds.h
Normal file
48
src/ui/SelectionBounds.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
|
||||||
|
class EntityAdmin;
|
||||||
|
class Simulation;
|
||||||
|
class WorldCoordinates;
|
||||||
|
struct FactoryState;
|
||||||
|
|
||||||
|
// Where selectable objects are on the screen. The world renderer draws every selection
|
||||||
|
// outline from these rectangles, and the selection panel is placed beside the one that
|
||||||
|
// covers the whole selection (REQ-UI-SELECTION-PANEL).
|
||||||
|
//
|
||||||
|
// All of them are in the game world view's own widget coordinates, and all of them are
|
||||||
|
// only true for the WorldCoordinates they were asked for: the transform is a value built
|
||||||
|
// per frame or per event, so a rectangle does not survive a scroll or a resize.
|
||||||
|
|
||||||
|
// The footprint of a building or of a construction site (REQ-UI-SELECTION-CARD). Null
|
||||||
|
// when the id names neither, which is what a deconstruction under the caller looks like.
|
||||||
|
std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
BuildingId id);
|
||||||
|
|
||||||
|
// The body of a ship or of a defence station (REQ-UI-ENTITY-CLICK-SELECT): a station's
|
||||||
|
// footprint, or the square the ship's triangle is drawn in.
|
||||||
|
std::optional<QRectF> getActorWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity actor);
|
||||||
|
|
||||||
|
// The circle a piece of debris is drawn as (REQ-UI-DEBRIS-CLICK-SELECT).
|
||||||
|
std::optional<QRectF> getDebrisWidgetRect(const EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity debris);
|
||||||
|
|
||||||
|
// The rectangle covering a whole selection -- one object, or the bounding box of all of
|
||||||
|
// them (REQ-UI-SELECTION-PANEL). Objects that no longer resolve are skipped; the result
|
||||||
|
// is null when none of them does.
|
||||||
|
QRect getSelectionWidgetRect(const Simulation& sim, const WorldCoordinates& coordinates,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris);
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
#include "SelectionPanel.h"
|
#include "SelectionPanel.h"
|
||||||
|
|
||||||
|
#include <QLayout>
|
||||||
|
#include <QList>
|
||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QScrollBar>
|
#include <QScrollBar>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "BuildingIconCache.h"
|
#include "BuildingIconCache.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
@@ -13,8 +18,8 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
// Distance kept between the panel and the edges of the band it is anchored to
|
// Distance kept between the panel and the edges of the game world view, and between it
|
||||||
// (REQ-UI-SELECTION-PANEL).
|
// and the widgets it steps around (REQ-UI-SELECTION-PANEL).
|
||||||
const int kMarginPx = 8;
|
const int kMarginPx = 8;
|
||||||
|
|
||||||
// Upper bound on the card width. The panel is content-sized, but several of the cards'
|
// Upper bound on the card width. The panel is content-sized, but several of the cards'
|
||||||
@@ -94,10 +99,10 @@ SelectionPanel::~SelectionPanel()
|
|||||||
unregisterForEvents();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::anchorTo(const QRect& bandRect)
|
void SelectionPanel::invalidateLayout()
|
||||||
{
|
{
|
||||||
m_bandRect = bandRect;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
updateVisibility();
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
@@ -113,6 +118,18 @@ void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> ev
|
|||||||
rebuildContent();
|
rebuildContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SelectionPanel::handleEvent(
|
||||||
|
std::shared_ptr<const SelectionAnchorChangedEvent> event)
|
||||||
|
{
|
||||||
|
// A new selection is starting. Both the anchor and the side are settled against it
|
||||||
|
// and then left alone for as long as it lasts (REQ-UI-SELECTION-PANEL); the side is
|
||||||
|
// only reset here, being resolved on the next placement once the card's width is
|
||||||
|
// known. The rect arrives in the world view's coordinates and is translated when the
|
||||||
|
// panel is placed, the two widgets being siblings in the same parent.
|
||||||
|
m_anchorRect = event->rectPx;
|
||||||
|
m_side.reset();
|
||||||
|
}
|
||||||
|
|
||||||
void SelectionPanel::handleEvent(
|
void SelectionPanel::handleEvent(
|
||||||
std::shared_ptr<const EntitySelectionChangedEvent> event)
|
std::shared_ptr<const EntitySelectionChangedEvent> event)
|
||||||
{
|
{
|
||||||
@@ -178,7 +195,7 @@ void SelectionPanel::refreshContent()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_content->refresh();
|
m_content->refresh();
|
||||||
updateVisibility();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::rebuildContent()
|
void SelectionPanel::rebuildContent()
|
||||||
@@ -208,27 +225,29 @@ void SelectionPanel::rebuildContent()
|
|||||||
m_content->show();
|
m_content->show();
|
||||||
m_content->refresh();
|
m_content->refresh();
|
||||||
}
|
}
|
||||||
updateVisibility();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::updateVisibility()
|
void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
|
||||||
{
|
{
|
||||||
// Nothing selected in either category means no panel at all rather than an empty one
|
// Nothing selected in either category means no panel at all rather than an empty one
|
||||||
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible. Before the
|
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible.
|
||||||
// owner has anchored the panel there is nowhere to put it either, so it stays hidden
|
setVisible(m_content != nullptr);
|
||||||
// until then.
|
if (m_content == nullptr || viewRect.isNull())
|
||||||
const bool shouldShow = (m_content != nullptr) && !m_bandRect.isNull();
|
|
||||||
setVisible(shouldShow);
|
|
||||||
if (shouldShow)
|
|
||||||
{
|
{
|
||||||
refit();
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void SelectionPanel::refit()
|
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
{
|
|
||||||
const QRect band =
|
// The anchor is published in the world view's coordinates and this panel is placed in
|
||||||
m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
// its parent's; the two widgets are siblings, so the view's own origin is the whole
|
||||||
|
// difference. Without an anchor the panel falls back to the top-right corner, by
|
||||||
|
// standing beside a point just outside that corner -- in practice unreachable, every
|
||||||
|
// non-empty selection following a click or a drag that publishes one.
|
||||||
|
const QRect anchorRect =
|
||||||
|
m_anchorRect.isNull() ? QRect(band.right() + kMarginPx + 1, band.top(), 1, 1)
|
||||||
|
: m_anchorRect.translated(viewRect.topLeft());
|
||||||
|
|
||||||
// The panel's border is drawn around the scroll area rather than around the card, so
|
// The panel's border is drawn around the scroll area rather than around the card, so
|
||||||
// it is added to whatever the card asks for. Spelled out here instead of read back
|
// it is added to whatever the card asks for. Spelled out here instead of read back
|
||||||
@@ -236,8 +255,7 @@ void SelectionPanel::refit()
|
|||||||
// style for it before the first show is unreliable.
|
// style for it before the first show is unreliable.
|
||||||
const int borderPx = 1;
|
const int borderPx = 1;
|
||||||
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
||||||
const int maxHeightPx = band.height() - 2 * borderPx;
|
if (maxWidthPx <= 0 || band.height() <= 2 * borderPx)
|
||||||
if (maxWidthPx <= 0 || maxHeightPx <= 0)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -247,9 +265,16 @@ void SelectionPanel::refit()
|
|||||||
// laid out, does the width it reports. Measuring at whatever width the panel
|
// laid out, does the width it reports. Measuring at whatever width the panel
|
||||||
// happens to have carries the previous card's shape into this one.
|
// happens to have carries the previous card's shape into this one.
|
||||||
//
|
//
|
||||||
// Each measurement re-runs the body layout. Cards are built and discarded whole, so
|
// Each measurement re-runs the card's layouts -- every one of them, not just the
|
||||||
// its cached hint describes the card before this one until it is invalidated, and
|
// body's own. Changing a label's text posts a LayoutRequest to the widget holding it
|
||||||
// re-running it is also what accounts for the parts a card hides and shows as it
|
// and that event is only delivered when the event loop next runs, so a layout nested
|
||||||
|
// inside the card still reports the width of the text before the change: measuring
|
||||||
|
// here and re-measuring on the following refresh then gives two different answers,
|
||||||
|
// and the panel visibly resizes a frame after its content changed. Re-activating
|
||||||
|
// them all is what a delivered LayoutRequest would have done.
|
||||||
|
//
|
||||||
|
// Cards are built and discarded whole, so this is also what discards the previous
|
||||||
|
// card's cached hints, and what accounts for the parts a card hides and shows as it
|
||||||
// refreshes. The polish belongs to the same step: a freshly created chip reports an
|
// refreshes. The polish belongs to the same step: a freshly created chip reports an
|
||||||
// unstyled hint until the stylesheet has reached it, and the chips carry border and
|
// unstyled hint until the stylesheet has reached it, and the chips carry border and
|
||||||
// padding that change their size.
|
// padding that change their size.
|
||||||
@@ -257,6 +282,16 @@ void SelectionPanel::refit()
|
|||||||
{
|
{
|
||||||
m_body->resize(widthPx, m_body->height());
|
m_body->resize(widthPx, m_body->height());
|
||||||
m_body->ensurePolished();
|
m_body->ensurePolished();
|
||||||
|
|
||||||
|
// Deepest first, so no layout is re-activated from children that are themselves
|
||||||
|
// still stale. findChildren walks parents before children, hence the reverse.
|
||||||
|
const QList<QLayout*> nested = m_body->findChildren<QLayout*>();
|
||||||
|
for (QList<QLayout*>::const_reverse_iterator it = nested.rbegin();
|
||||||
|
it != nested.rend(); ++it)
|
||||||
|
{
|
||||||
|
(*it)->invalidate();
|
||||||
|
(*it)->activate();
|
||||||
|
}
|
||||||
m_body->layout()->invalidate();
|
m_body->layout()->invalidate();
|
||||||
m_body->layout()->activate();
|
m_body->layout()->activate();
|
||||||
return m_body->sizeHint();
|
return m_body->sizeHint();
|
||||||
@@ -273,9 +308,35 @@ void SelectionPanel::refit()
|
|||||||
// First at the cap, the most room the card can ever get, to learn how wide it
|
// First at the cap, the most room the card can ever get, to learn how wide it
|
||||||
// wants to be; then at that width for the height that follows from it.
|
// wants to be; then at that width for the height that follows from it.
|
||||||
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
|
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
|
||||||
|
|
||||||
|
// Which side of the selection the panel takes is settled on the first placement
|
||||||
|
// after a new anchor and kept for as long as that selection lasts, so a card that
|
||||||
|
// grows or shrinks never flips the panel across the object it describes
|
||||||
|
// (REQ-UI-SELECTION-PANEL). This is the first point at which its width is known.
|
||||||
|
if (!m_side.has_value())
|
||||||
|
{
|
||||||
|
m_side = chooseSide(band, anchorRect, contentWidthPx + 2 * borderPx,
|
||||||
|
kMarginPx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// How much height there is depends on where the panel ends up standing: of the
|
||||||
|
// widgets placed before it, only those whose rectangles meet its own column are
|
||||||
|
// in its way. That column follows from the width just measured, so this cannot be
|
||||||
|
// settled before it -- and where the scroll bar below widens the panel, the second
|
||||||
|
// pass settles it again against the wider column. Asking for the whole band's
|
||||||
|
// height is what makes the answer the most the panel could have there.
|
||||||
|
const int maxHeightPx =
|
||||||
|
placeBesideAnchor(band, anchorRect, *m_side,
|
||||||
|
QSize(contentWidthPx + 2 * borderPx, band.height()),
|
||||||
|
occupiedRects, kMarginPx).height() - 2 * borderPx;
|
||||||
|
if (maxHeightPx <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int contentHeightPx = measureAt(contentWidthPx).height();
|
int contentHeightPx = measureAt(contentWidthPx).height();
|
||||||
|
|
||||||
// A card taller than the band is capped there and scrolls
|
// A card taller than the space left is capped there and scrolls
|
||||||
// (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel
|
// (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel
|
||||||
// widens by its width to leave the card the width its height was measured for --
|
// widens by its width to leave the card the width its height was measured for --
|
||||||
// and where the cap does not allow that, the card is measured again at what is
|
// and where the cap does not allow that, the card is measured again at what is
|
||||||
@@ -301,11 +362,8 @@ void SelectionPanel::refit()
|
|||||||
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
||||||
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
||||||
|
|
||||||
// Right-aligned in the band and centered on it vertically. The band excludes the
|
setGeometry(placeBesideAnchor(band, anchorRect, *m_side,
|
||||||
// build button bar's strip, so centering here never puts the panel over the bar
|
QSize(panelWidthPx, panelHeightPx),
|
||||||
// (REQ-UI-SELECTION-PANEL).
|
occupiedRects, kMarginPx));
|
||||||
setGeometry(band.right() - panelWidthPx + 1,
|
|
||||||
band.top() + (band.height() - panelHeightPx) / 2,
|
|
||||||
panelWidthPx, panelHeightPx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,17 @@
|
|||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "DebrisSelectionChangedEvent.h"
|
#include "DebrisSelectionChangedEvent.h"
|
||||||
#include "DebugDrawToggledEvent.h"
|
#include "DebugDrawToggledEvent.h"
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "PlayerCommandsAppliedEvent.h"
|
#include "PlayerCommandsAppliedEvent.h"
|
||||||
|
#include "SelectionAnchorChangedEvent.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
#include "selection/SelectionContentFactory.h"
|
#include "selection/SelectionContentFactory.h"
|
||||||
@@ -28,14 +34,16 @@ class QVBoxLayout;
|
|||||||
// What each card looks like lives in src/ui/selection/.
|
// 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
|
// 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 card, anchors to the right edge of
|
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, places itself in what the build
|
||||||
// the band its owner hands it, and hides itself entirely while nothing is selected
|
// button bar and the controls panel have left free, and hides itself entirely while
|
||||||
// (REQ-UI-EMPTY-SELECTION).
|
// nothing is selected (REQ-UI-EMPTY-SELECTION).
|
||||||
class SelectionPanel : public QWidget,
|
class SelectionPanel : public QWidget,
|
||||||
|
public FloatingPanel,
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
PlayerCommandsAppliedEvent,
|
PlayerCommandsAppliedEvent,
|
||||||
EntitySelectionChangedEvent,
|
EntitySelectionChangedEvent,
|
||||||
SelectionChangedEvent,
|
SelectionChangedEvent,
|
||||||
|
SelectionAnchorChangedEvent,
|
||||||
DebrisSelectionChangedEvent,
|
DebrisSelectionChangedEvent,
|
||||||
DebugDrawToggledEvent>
|
DebugDrawToggledEvent>
|
||||||
{
|
{
|
||||||
@@ -49,14 +57,16 @@ public:
|
|||||||
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
||||||
~SelectionPanel() override;
|
~SelectionPanel() override;
|
||||||
|
|
||||||
// Confines the panel to the given band of the game world view: it right-aligns
|
// Sizes the panel to its card and places it beside the selection it describes, in
|
||||||
// within it and centers vertically in it. The band is the world view less the strip
|
// what the widgets placed before it have left free. Keeping clear of them is entirely
|
||||||
// the build button bar occupies, so the two never overlap and the bar never has to
|
// this panel's job; neither of them ever moves for it (REQ-UI-SELECTION-PANEL,
|
||||||
// move (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
// REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL).
|
||||||
void anchorTo(const QRect& bandRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const SelectionAnchorChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> 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 DebrisSelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
@@ -69,11 +79,9 @@ private:
|
|||||||
void refreshContent();
|
void refreshContent();
|
||||||
// Replaces the card with the one the current selection calls for.
|
// Replaces the card with the one the current selection calls for.
|
||||||
void rebuildContent();
|
void rebuildContent();
|
||||||
// Shows the panel while a card exists and hides it otherwise
|
// Asks for the placement pass to be re-run, the card having changed size or the
|
||||||
// (REQ-UI-EMPTY-SELECTION), re-fitting it to the card while it is shown.
|
// panel having gained or lost its reason to be shown at all.
|
||||||
void updateVisibility();
|
void invalidateLayout();
|
||||||
// Re-fits the panel to its card within the anchored band.
|
|
||||||
void refit();
|
|
||||||
|
|
||||||
SelectionContext m_context;
|
SelectionContext m_context;
|
||||||
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
||||||
@@ -84,12 +92,18 @@ private:
|
|||||||
ContentKey m_contentKey;
|
ContentKey m_contentKey;
|
||||||
SelectionContent* m_content = nullptr;
|
SelectionContent* m_content = nullptr;
|
||||||
|
|
||||||
// The band the panel confines itself to, in the coordinates of its parent; null
|
// Where the current selection was on the screen when it started, in the game world
|
||||||
// until the owner has anchored it for the first time.
|
// view's coordinates, and which side of it the panel took. Both are frozen for as
|
||||||
QRect m_bandRect;
|
// long as the selection lasts: the anchor because the panel does not chase a
|
||||||
|
// scrolling view or a moving ship, the side because a card that grows must not flip
|
||||||
|
// the panel across the object (REQ-UI-SELECTION-PANEL). The side is resolved on the
|
||||||
|
// first placement after a new anchor, being the first point at which the panel's
|
||||||
|
// width is known.
|
||||||
|
QRect m_anchorRect;
|
||||||
|
std::optional<PanelSide> m_side;
|
||||||
|
|
||||||
// Scrolls the card once it outgrows the band (REQ-UI-SELECTION-PANEL). The card is a
|
// Scrolls the card once it outgrows the space the panel has (REQ-UI-SELECTION-PANEL).
|
||||||
// child of m_body, not of the panel itself.
|
// The card is a child of m_body, not of the panel itself.
|
||||||
QScrollArea* m_scrollArea;
|
QScrollArea* m_scrollArea;
|
||||||
QWidget* m_body;
|
QWidget* m_body;
|
||||||
QVBoxLayout* m_bodyLayout;
|
QVBoxLayout* m_bodyLayout;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
|
#include "SelectionBounds.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
@@ -108,7 +109,7 @@ QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
WorldRenderer::WorldRenderer(Simulation& sim, const VisualsConfig& visuals,
|
WorldRenderer::WorldRenderer(const Simulation& sim, const VisualsConfig& visuals,
|
||||||
ItemIconCache* itemIcons, const std::string& configDir)
|
ItemIconCache* itemIcons, const std::string& configDir)
|
||||||
: m_sim(sim)
|
: m_sim(sim)
|
||||||
, m_visuals(visuals)
|
, m_visuals(visuals)
|
||||||
@@ -451,30 +452,6 @@ void WorldRenderer::drawBuildings(QPainter& painter, const WorldCoordinates& coo
|
|||||||
drawSelectionHighlights(painter, coordinates, frame);
|
drawSelectionHighlights(painter, coordinates, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<QRectF> WorldRenderer::footprintWidgetRect(
|
|
||||||
const WorldCoordinates& coordinates, BuildingId id) const
|
|
||||||
{
|
|
||||||
std::optional<QPoint> anchor;
|
|
||||||
std::optional<QSize> footprint;
|
|
||||||
|
|
||||||
if (const Building* b = findBuilding(m_sim.getFactoryState(), id))
|
|
||||||
{
|
|
||||||
anchor = b->anchor;
|
|
||||||
footprint = b->footprint;
|
|
||||||
}
|
|
||||||
else if (const ConstructionSite* s = findSite(m_sim.getFactoryState(), id))
|
|
||||||
{
|
|
||||||
anchor = s->anchor;
|
|
||||||
footprint = s->footprint;
|
|
||||||
}
|
|
||||||
if (!anchor.has_value() || !footprint.has_value()) { return std::nullopt; }
|
|
||||||
|
|
||||||
const QPointF tl = coordinates.tileToWidget(*anchor);
|
|
||||||
return QRectF(tl.x(), tl.y(),
|
|
||||||
footprint->width() * static_cast<qreal>(coordinates.getTilePx()),
|
|
||||||
footprint->height() * static_cast<qreal>(coordinates.getTilePx()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
|
void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
|
||||||
const WorldRenderFrame& frame)
|
const WorldRenderFrame& frame)
|
||||||
{
|
{
|
||||||
@@ -483,7 +460,8 @@ void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordi
|
|||||||
|
|
||||||
for (BuildingId selId : frame.selection.getSelectedBuildings())
|
for (BuildingId selId : frame.selection.getSelectedBuildings())
|
||||||
{
|
{
|
||||||
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, selId);
|
const std::optional<QRectF> rect =
|
||||||
|
getBuildingWidgetRect(m_sim.getFactoryState(), coordinates, selId);
|
||||||
if (!rect.has_value()) { continue; }
|
if (!rect.has_value()) { continue; }
|
||||||
// Outline sits 1px outside the footprint (into adjacent tiles).
|
// Outline sits 1px outside the footprint (into adjacent tiles).
|
||||||
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public:
|
|||||||
// `itemIcons` is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
|
// `itemIcons` is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not
|
||||||
// owned, must outlive this renderer. `configDir` is used once, to load the
|
// owned, must outlive this renderer. `configDir` is used once, to load the
|
||||||
// per-building world icons.
|
// per-building world icons.
|
||||||
WorldRenderer(Simulation& sim, const VisualsConfig& visuals,
|
WorldRenderer(const Simulation& sim, const VisualsConfig& visuals,
|
||||||
ItemIconCache* itemIcons, const std::string& configDir);
|
ItemIconCache* itemIcons, const std::string& configDir);
|
||||||
~WorldRenderer();
|
~WorldRenderer();
|
||||||
|
|
||||||
@@ -139,16 +139,10 @@ private:
|
|||||||
BuildingType type, const QRectF& box,
|
BuildingType type, const QRectF& box,
|
||||||
const QColor& fill) const;
|
const QColor& fill) const;
|
||||||
|
|
||||||
// Widget-space rectangle covering a building or construction site's footprint,
|
|
||||||
// or nullopt if the id resolves to neither. Used by the selection highlight.
|
|
||||||
std::optional<QRectF> footprintWidgetRect(const WorldCoordinates& coordinates,
|
|
||||||
BuildingId id) const;
|
|
||||||
|
|
||||||
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
||||||
|
|
||||||
// Non-const only because EntityAdmin's component accessors are; the renderer
|
// The renderer reads the simulation and never writes it.
|
||||||
// reads the simulation and never writes it.
|
const Simulation& m_sim;
|
||||||
Simulation& m_sim;
|
|
||||||
const VisualsConfig& m_visuals;
|
const VisualsConfig& m_visuals;
|
||||||
|
|
||||||
// Per-item icon cache (REQ-UI-ITEM-ICON), shared window-wide and owned by
|
// Per-item icon cache (REQ-UI-ITEM-ICON), shared window-wide and owned by
|
||||||
|
|||||||
@@ -41,13 +41,22 @@ BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target.building || !target.building->production.has_value())
|
// Which recipe describes the cycle: the one running, or -- between cycles -- the one
|
||||||
|
// that ran last. Dropping it while idle would take the summary row and the chips'
|
||||||
|
// per-cycle amounts away and bring them back with every cycle, resizing the card in
|
||||||
|
// step with the building's status (REQ-UI-RECIPE-SUMMARY). Only a building that has
|
||||||
|
// never run has nothing to describe.
|
||||||
|
if (target.building && target.building->production.has_value())
|
||||||
|
{
|
||||||
|
m_lastRecipeId = target.building->production->recipeId;
|
||||||
|
}
|
||||||
|
if (!target.building || m_lastRecipeId.empty())
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
|
const RecipeDef* recipe =
|
||||||
target.building->production->recipeId, target.type);
|
getContext().config->recipes.findRecipeDef(m_lastRecipeId, target.type);
|
||||||
if (!recipe)
|
if (!recipe)
|
||||||
{
|
{
|
||||||
return info;
|
return info;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "BufferedBuildingContent.h"
|
#include "BufferedBuildingContent.h"
|
||||||
#include "SelectionContentFactory.h"
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
// The card for a Smelter or a Reprocessing Plant (REQ-UI-SELECTION-CONTENT). Both
|
// The card for a Smelter or a Reprocessing Plant (REQ-UI-SELECTION-CONTENT). Both
|
||||||
// auto-process whatever they receive and have no player-facing recipe selection
|
// auto-process whatever they receive and have no player-facing recipe selection
|
||||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), so the card has no configuration group at
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), so the card has no configuration group at
|
||||||
// all; its cycle is whichever recipe is currently in production.
|
// all; its cycle is whichever recipe is in production, or the last one that was.
|
||||||
class AutoProductionContent : public BufferedBuildingContent
|
class AutoProductionContent : public BufferedBuildingContent
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -17,4 +19,11 @@ public:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The recipe last seen in production, which keeps describing the cycle while the
|
||||||
|
// building sits between cycles (REQ-UI-RECIPE-SUMMARY). Mutable because it is a
|
||||||
|
// record of what getCycleInfo() has observed rather than state of its own: the card
|
||||||
|
// shows the same thing whether or not it has been asked before.
|
||||||
|
mutable std::string m_lastRecipeId;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,8 +44,9 @@ protected:
|
|||||||
// REQ-BLD-SALVAGE-BAY) or has no recipe or schematic selected yet: the
|
// REQ-BLD-SALVAGE-BAY) or has no recipe or schematic selected yet: the
|
||||||
// production section is then not shown (REQ-UI-PRODUCTION-PROGRESS).
|
// production section is then not shown (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
bool runsProduction = false;
|
bool runsProduction = false;
|
||||||
// 0 while an auto-recipe building sits between cycles, when no single recipe
|
// 0 only when no recipe describes the cycle at all -- an auto-recipe building
|
||||||
// names a cycle time; the progress line then reads "idle".
|
// that has yet to run one (REQ-UI-RECIPE-SUMMARY). The progress line reads
|
||||||
|
// "idle" whenever no cycle is actually running, whatever this says.
|
||||||
double durationSeconds = 0.0;
|
double durationSeconds = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user