float the build buttons as a bar over the game world
Implements the REQ-UI-BUILD-BAR rewrite: the build buttons leave the side panel column for a widget floating at the bottom center of the game world view, as one non-wrapping row. BuildButtonGrid is renamed BuildButtonBar and its QGridLayout becomes a QHBoxLayout; the side panel column is left with two equal-height panels. Buttons become icon-only. Each face is composed into a single pixmap per QIcon mode - hotkey badge in the top-left corner, chip icon centered, cost and block icon below - because a QPushButton holds only one icon. That replaces the custom-painted BuildButton and lets Qt grey an unaffordable button by swapping the pixmap. The building name moves into the tooltip, which now always leads with it, and a missing chip SVG falls back to the name in the chip's place. The badge labels come from InputMapper, searched out of the same table the key handler uses so a badge cannot claim a key that does nothing. The Deconstruct button has no cost, so it shows its name there instead, and sits last behind a gap. Parenting the bar to MainWindow makes creation order the stacking order: it lands above the world view and its vignettes and below the modal dim, and Qt routes mouse events to it rather than the world, which is the whole input clause of REQ-UI-BUILD-BAR at no cost. Requirements are amended for the two things the mockup added: the hotkey badge and the Deconstruct caption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
@@ -95,7 +95,7 @@ Schematic drops: when an enemy station set is destroyed, the simulation generate
|
|||||||
|
|
||||||
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, deconstruct mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
|
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, deconstruct mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
|
||||||
|
|
||||||
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonGrid`).
|
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonBar` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonBar`).
|
||||||
|
|
||||||
### Reading Simulation State
|
### Reading Simulation State
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
|||||||
Three product targets plus tests:
|
Three product targets plus tests:
|
||||||
|
|
||||||
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
||||||
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selected building panel, build button grid. Depends on `lib` and on Qt's OpenGL widgets module.
|
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selected building panel, build button bar. Depends on `lib` and on Qt's OpenGL widgets module.
|
||||||
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
||||||
- `tests/` — Catch2 tests. Links only against `lib`.
|
- `tests/` — Catch2 tests. Links only against `lib`.
|
||||||
|
|
||||||
|
|||||||
@@ -556,11 +556,12 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- REQ-UI-BUILD-BAR: All placeable building types are shown as a **single horizontal row** of buttons with no grouping and no wrapping, inside a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), horizontally centered and anchored at the bottom edge with a small margin. Tunnel Entry and Tunnel Exit share a single **Tunnel** button (REQ-BLD-TUNNEL-MODE) rather than one button each. The bar is sized to its buttons and re-centers whenever the set of shown buttons changes (REQ-LOCK-BUILDING) or the view is resized.
|
- REQ-UI-BUILD-BAR: All placeable building types are shown as a **single horizontal row** of buttons with no grouping and no wrapping, inside a widget that **floats over the game world view** (REQ-UI-WORLD-SIZE), horizontally centered and anchored at the bottom edge with a small margin. Tunnel Entry and Tunnel Exit share a single **Tunnel** button (REQ-BLD-TUNNEL-MODE) rather than one button each. The bar is sized to its buttons and re-centers whenever the set of shown buttons changes (REQ-LOCK-BUILDING) or the view is resized.
|
||||||
- **Overlay behavior.** The bar occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected (the world is not inset for the bar). The bar is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full 100-pixel bottom band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the bar.
|
- **Overlay behavior.** The bar occludes the strip of the game world it covers; the world view itself keeps its full extent and the view's scrolling, ghost rendering, and tile geometry are unaffected (the world is not inset for the bar). The bar is drawn above the pause and deconstruct vignettes (REQ-UI-PAUSE-BORDER, REQ-UI-DECONSTRUCT-BORDER), which keep their full 100-pixel bottom band underneath it, and below the modal dim (REQ-UI-MODAL-DIM), which covers the entire game window including the bar.
|
||||||
- **Input.** Mouse events over the bar are consumed by the bar and never reach the game world: hovering it shows no builder-mode ghost at the tile beneath, and clicking it neither places a building nor changes the selection. Right-clicking the bar does not exit builder mode (REQ-BLD-BUILDER-MODE) or cancel a belt drag (REQ-BLD-BELT-DRAG).
|
- **Input.** Mouse events over the bar are consumed by the bar and never reach the game world: hovering it shows no builder-mode ghost at the tile beneath, and clicking it neither places a building nor changes the selection. Right-clicking the bar does not exit builder mode (REQ-BLD-BUILDER-MODE) or cancel a belt drag (REQ-BLD-BELT-DRAG).
|
||||||
- REQ-UI-BUILD-COST: Each button is **icon-only with a cost**: it shows the building's icon (REQ-UI-BUILD-ICON) and its building block cost with the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) in place of the trailing `Blocks` word, e.g. `2` then a small block icon. The building name is not shown on the button; it is shown in the button's hover tooltip instead (REQ-UI-BUILD-TOOLTIP). When no icon file exists for `building_block`, the cost is shown as the bare number. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) has no cost and shows its icon alone.
|
- REQ-UI-BUILD-COST: Each button is **icon-only with a cost**, its face composed of three elements: the button's **hotkey badge** in the top-left corner, the building's icon (REQ-UI-BUILD-ICON) centered below it, and the building block cost centered under the icon, shown with the `building_block` item icon (REQ-UI-BLOCKS-ICON, REQ-UI-ITEM-ICON) to the right of the number in place of the trailing `Blocks` word, e.g. `2` then a small block icon. The building name is not shown on the button; it is shown in the button's hover tooltip instead (REQ-UI-BUILD-TOOLTIP). When no icon file exists for `building_block`, the cost is shown as the bare number. The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) has no cost and shows its name as a text caption in the cost's place.
|
||||||
|
- **Hotkey badge.** The badge names the build hotkey that activates the button (REQ-UI-HOTKEYS), so the player can learn the shortcuts from the bar itself. It is rendered smaller and dimmer than the cost so it reads as secondary. A plain-digit hotkey is shown as the bare digit (`1`, `2`, `3`); a Shift+digit hotkey is shown with the shift glyph prefixed and no separator (`⇧1` … `⇧6`); the Deconstruct button shows `Q`. A button whose building type has no build hotkey shows no badge and keeps the same face size, so the row stays even.
|
||||||
- REQ-UI-BUILD-ICON: Each build button shows an icon. Icons are SVG files loaded at runtime from `data/icons/buildings/` (a sibling of the config directory, read the same way as `visuals.toml`), one file per button named after the building's id (e.g. `belt.svg`, `reprocessing_plant.svg`). The shared Tunnel button (REQ-UI-BUILD-BAR) uses `tunnel_entry.svg`; the Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) uses `deconstruct.svg`. Each icon is a rounded colored "chip" bearing a white line glyph, the chip color following the building's fill color in `visuals.toml`. A missing icon file leaves the button showing its building name as a text caption in place of the icon, so the button stays identifiable in the icon-only bar (REQ-UI-BUILD-COST); it is not an error.
|
- REQ-UI-BUILD-ICON: Each build button shows an icon. Icons are SVG files loaded at runtime from `data/icons/buildings/` (a sibling of the config directory, read the same way as `visuals.toml`), one file per button named after the building's id (e.g. `belt.svg`, `reprocessing_plant.svg`). The shared Tunnel button (REQ-UI-BUILD-BAR) uses `tunnel_entry.svg`; the Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) uses `deconstruct.svg`. Each icon is a rounded colored "chip" bearing a white line glyph, the chip color following the building's fill color in `visuals.toml`. A missing icon file leaves the button showing its building name as a text caption in place of the icon, so the button stays identifiable in the icon-only bar (REQ-UI-BUILD-COST); it is not an error.
|
||||||
- REQ-UI-BUILD-TOOLTIP: Each building-type button shows a hover tooltip consisting of the building name followed by the descriptive text defined for that building type in `buildings.toml` (the optional per-building tooltip field). Because the button caption is icon-only (REQ-UI-BUILD-COST), the name is always part of the tooltip; if a building type defines no tooltip text, the tooltip shows the name alone. This tooltip is distinct from the recipe/schematic selection tooltip (REQ-UI-SELECT-TOOLTIP). The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) is not a building type and so has no config-defined tooltip; it instead shows its own refund tooltip defined in REQ-UI-DECONSTRUCT-BUTTON.
|
- REQ-UI-BUILD-TOOLTIP: Each building-type button shows a hover tooltip consisting of the building name followed by the descriptive text defined for that building type in `buildings.toml` (the optional per-building tooltip field). Because the button caption is icon-only (REQ-UI-BUILD-COST), the name is always part of the tooltip; if a building type defines no tooltip text, the tooltip shows the name alone. This tooltip is distinct from the recipe/schematic selection tooltip (REQ-UI-SELECT-TOOLTIP). The Deconstruct button (REQ-UI-DECONSTRUCT-BUTTON) is not a building type and so has no config-defined tooltip; it instead shows its own refund tooltip defined in REQ-UI-DECONSTRUCT-BUTTON.
|
||||||
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled. A disabled button's icon (REQ-UI-BUILD-ICON) is rendered in a greyed variant, with its colored chip background recolored grey while the white glyph is retained.
|
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled. A disabled button's icon (REQ-UI-BUILD-ICON) is rendered in a greyed variant, with its colored chip background recolored grey while the white glyph is retained.
|
||||||
- REQ-UI-DECONSTRUCT-BUTTON: A dedicated **Deconstruct** button is shown in the build button bar (REQ-UI-BUILD-BAR), as the last entry of the row and **visually separated** from the building-type buttons by a gap (not a divider line), because it toggles a mode rather than selecting a building type. Clicking it toggles deconstruct mode on and off, equivalent to the Q deconstruct toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while deconstruct mode is active. The button shows a hover tooltip stating the deconstruction refund (REQ-BLD-DECONSTRUCT): that deconstructing a fully-built building returns `world.toml [world].refund_percentage` percent of its building block cost once deconstruction completes, and that a construction site removed before it finishes building is refunded in full. When `refund_percentage` is 100% both cases yield the same refund, and the tooltip is simplified to state the single refund percentage without distinguishing the two cases. Unlike the building-type button tooltips (REQ-UI-BUILD-TOOLTIP), this tooltip is not config-defined text but is composed from the refund percentage.
|
- REQ-UI-DECONSTRUCT-BUTTON: A dedicated **Deconstruct** button is shown in the build button bar (REQ-UI-BUILD-BAR), as the last entry of the row and **visually separated** from the building-type buttons by a gap (not a divider line), because it toggles a mode rather than selecting a building type. Its face follows REQ-UI-BUILD-COST with two differences: its hotkey badge reads `Q`, and because it has no building block cost it shows its **Deconstruct** name as a text caption where the building-type buttons show their cost — so it is the one labelled button in the bar. It is therefore wider than the building-type buttons, which share a uniform width. Clicking it toggles deconstruct mode on and off, equivalent to the Q deconstruct toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while deconstruct mode is active. The button shows a hover tooltip stating the deconstruction refund (REQ-BLD-DECONSTRUCT): that deconstructing a fully-built building returns `world.toml [world].refund_percentage` percent of its building block cost once deconstruction completes, and that a construction site removed before it finishes building is refunded in full. When `refund_percentage` is 100% both cases yield the same refund, and the tooltip is simplified to state the single refund percentage without distinguishing the two cases. Unlike the building-type button tooltips (REQ-UI-BUILD-TOOLTIP), this tooltip is not config-defined text but is composed from the refund percentage.
|
||||||
|
|
||||||
### Blueprint Panel
|
### Blueprint Panel
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
// Emitted when the set of unlocked building types changes (REQ-LOCK-BUILDING),
|
// Emitted when the set of unlocked building types changes (REQ-LOCK-BUILDING),
|
||||||
// i.e. after an unlock group granting a building is awarded (REQ-DEF-SCHEMATIC-DROP)
|
// i.e. after an unlock group granting a building is awarded (REQ-DEF-SCHEMATIC-DROP)
|
||||||
// or on Restart. The build button grid re-evaluates which buttons are shown.
|
// or on Restart. The build button bar re-evaluates which buttons are shown.
|
||||||
class UnlockedBuildingsChangedEvent : public Event
|
class UnlockedBuildingsChangedEvent : public Event
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|||||||
465
src/ui/BuildButtonBar.cpp
Normal file
465
src/ui/BuildButtonBar.cpp
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
#include "BuildButtonBar.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QRect>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSignalMapper>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QString>
|
||||||
|
#include <QSvgRenderer>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "BuildingTypeSelectedEvent.h"
|
||||||
|
#include "DeconstructModeToggleRequestedEvent.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
|
#include "IconCaption.h"
|
||||||
|
#include "InputMapper.h"
|
||||||
|
#include "ItemIconCache.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Size the SVG chips are drawn at on a button face, in device-independent pixels.
|
||||||
|
const QSize kIconSize(32, 32);
|
||||||
|
|
||||||
|
// Minimum width of a button face. The building-type buttons all come out at this
|
||||||
|
// width, so the row is uniform; a face whose caption is wider than this — the
|
||||||
|
// Deconstruct button's name — grows to fit rather than clipping
|
||||||
|
// (REQ-UI-DECONSTRUCT-BUTTON).
|
||||||
|
const int kFaceMinWidthPx = 48;
|
||||||
|
|
||||||
|
// Gap between the last building button and the Deconstruct button, which toggles
|
||||||
|
// a mode rather than selecting a building type (REQ-UI-DECONSTRUCT-BUTTON).
|
||||||
|
const int kDeconstructGapPx = 16;
|
||||||
|
|
||||||
|
// Distance from the bar to the bottom edge of the game world view
|
||||||
|
// (REQ-UI-BUILD-BAR).
|
||||||
|
const int kBottomMarginPx = 8;
|
||||||
|
|
||||||
|
// Gap between the chip icon and the cost line on a button face.
|
||||||
|
const int kFaceGapPx = 2;
|
||||||
|
|
||||||
|
// Rasterizes a chip SVG straight at its on-screen size times the device pixel
|
||||||
|
// ratio, so it stays crisp without a downscale step.
|
||||||
|
QPixmap renderChip(const QByteArray& svg)
|
||||||
|
{
|
||||||
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
QSvgRenderer renderer(svg);
|
||||||
|
QPixmap pixmap(static_cast<int>(kIconSize.width() * dpr),
|
||||||
|
static_cast<int>(kIconSize.height() * dpr));
|
||||||
|
pixmap.setDevicePixelRatio(dpr);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
renderer.render(&painter);
|
||||||
|
return pixmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal and grey-background chip pixmaps for a "<id>.svg" file. Empty pixmaps if
|
||||||
|
// the file cannot be read; the caller then falls back to a name caption
|
||||||
|
// (REQ-UI-BUILD-ICON).
|
||||||
|
struct ChipPixmaps { QPixmap normal; QPixmap grey; };
|
||||||
|
ChipPixmaps loadChipPixmaps(const QString& path)
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) { return {}; }
|
||||||
|
const QByteArray svg = file.readAll();
|
||||||
|
|
||||||
|
ChipPixmaps result;
|
||||||
|
result.normal = renderChip(svg);
|
||||||
|
|
||||||
|
// Recolor only the chip background: the first "#rrggbb" fill in the file is the
|
||||||
|
// rounded background rect; the white glyph uses fill="none" and is left alone.
|
||||||
|
QString greyed = QString::fromUtf8(svg);
|
||||||
|
static const QRegularExpression fillPattern(QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
|
||||||
|
const QRegularExpressionMatch match = fillPattern.match(greyed);
|
||||||
|
if (match.hasMatch())
|
||||||
|
{
|
||||||
|
greyed.replace(match.capturedStart(), match.capturedLength(),
|
||||||
|
QStringLiteral("fill=\"#5f636e\""));
|
||||||
|
}
|
||||||
|
result.grey = renderChip(greyed.toUtf8());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pixmap's size in device-independent pixels. The pixmaps composed here are
|
||||||
|
// rasterized at the device pixel ratio, so their raw size is not their layout size.
|
||||||
|
QSize getLogicalSize(const QPixmap& pixmap)
|
||||||
|
{
|
||||||
|
if (pixmap.isNull()) { return QSize(0, 0); }
|
||||||
|
const qreal dpr = pixmap.devicePixelRatio();
|
||||||
|
return QSize(static_cast<int>(pixmap.width() / dpr),
|
||||||
|
static_cast<int>(pixmap.height() / dpr));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One button face: the hotkey badge in the top-left corner, the chip icon centered
|
||||||
|
// below it, and the caption — the cost, or the Deconstruct name — centered at the
|
||||||
|
// bottom (REQ-UI-BUILD-COST). The three are composed into a single pixmap because
|
||||||
|
// a QPushButton holds only one icon.
|
||||||
|
QPixmap composeButtonFace(const QString& hotkeyLabel, const QPixmap& chip,
|
||||||
|
const QPixmap& caption, const QFont& badgeFont,
|
||||||
|
const QColor& badgeColor)
|
||||||
|
{
|
||||||
|
const QSize chipSize = getLogicalSize(chip);
|
||||||
|
const QSize captionSize = getLogicalSize(caption);
|
||||||
|
// The badge row is kept even for a building type without a hotkey, so buttons
|
||||||
|
// stay the same height and the row reads as one strip (REQ-UI-BUILD-COST).
|
||||||
|
const int badgeHeight = QFontMetrics(badgeFont).height();
|
||||||
|
const int width = qMax(kFaceMinWidthPx, qMax(chipSize.width(), captionSize.width()));
|
||||||
|
const int height = badgeHeight + chipSize.height() + kFaceGapPx + captionSize.height();
|
||||||
|
|
||||||
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
QPixmap face(static_cast<int>(width * dpr), static_cast<int>(height * dpr));
|
||||||
|
face.setDevicePixelRatio(dpr);
|
||||||
|
face.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&face);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
if (!hotkeyLabel.isEmpty())
|
||||||
|
{
|
||||||
|
painter.setFont(badgeFont);
|
||||||
|
painter.setPen(badgeColor);
|
||||||
|
painter.drawText(QRect(0, 0, width, badgeHeight),
|
||||||
|
Qt::AlignLeft | Qt::AlignVCenter, hotkeyLabel);
|
||||||
|
}
|
||||||
|
painter.drawPixmap((width - chipSize.width()) / 2, badgeHeight, chip);
|
||||||
|
painter.drawPixmap((width - captionSize.width()) / 2,
|
||||||
|
badgeHeight + chipSize.height() + kFaceGapPx, caption);
|
||||||
|
return face;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A composed button face and the size to show it at; the button needs both, and
|
||||||
|
// only the composer knows the size it arrived at.
|
||||||
|
struct ButtonFace { QIcon icon; QSize size; };
|
||||||
|
|
||||||
|
// The two-mode face of one build button. The modes differ only in the chip variant
|
||||||
|
// and the text color, so a disabled (unaffordable) button greys itself when Qt
|
||||||
|
// swaps the pixmap, with no extra work in updateAffordability()
|
||||||
|
// (REQ-UI-BUILD-DISABLED).
|
||||||
|
ButtonFace buildButtonFace(const ChipPixmaps& chip, const QString& hotkeyLabel,
|
||||||
|
const QString& name, const QString& captionText,
|
||||||
|
const QPixmap& blockIcon, const QFont& font,
|
||||||
|
const QPalette& palette)
|
||||||
|
{
|
||||||
|
QFont badgeFont = font;
|
||||||
|
if (font.pointSize() > 0)
|
||||||
|
{
|
||||||
|
badgeFont.setPointSize(qMax(1, font.pointSize() - 2));
|
||||||
|
}
|
||||||
|
else if (font.pixelSize() > 0)
|
||||||
|
{
|
||||||
|
badgeFont.setPixelSize(qMax(1, font.pixelSize() - 2));
|
||||||
|
}
|
||||||
|
// The badge is a reminder, not the button's identity, so it stays dimmed in
|
||||||
|
// both modes rather than competing with the cost.
|
||||||
|
const QColor badgeColor = palette.color(QPalette::Disabled, QPalette::ButtonText);
|
||||||
|
|
||||||
|
ButtonFace result;
|
||||||
|
for (QIcon::Mode mode : { QIcon::Normal, QIcon::Disabled })
|
||||||
|
{
|
||||||
|
const bool enabled = (mode == QIcon::Normal);
|
||||||
|
const QColor textColor = palette.color(
|
||||||
|
enabled ? QPalette::Active : QPalette::Disabled, QPalette::ButtonText);
|
||||||
|
const QPixmap& chipPixmap = enabled ? chip.normal : chip.grey;
|
||||||
|
// A missing "<id>.svg" puts the building name where the chip would be, so
|
||||||
|
// the button stays identifiable in an icon-only bar (REQ-UI-BUILD-ICON).
|
||||||
|
const QPixmap middle = chipPixmap.isNull()
|
||||||
|
? renderCaptionWithIcon(name, QPixmap(), font, textColor)
|
||||||
|
: chipPixmap;
|
||||||
|
const QPixmap caption =
|
||||||
|
renderCaptionWithIcon(captionText, blockIcon, font, textColor);
|
||||||
|
const QPixmap face =
|
||||||
|
composeButtonFace(hotkeyLabel, middle, caption, badgeFont, badgeColor);
|
||||||
|
result.icon.addPixmap(face, mode);
|
||||||
|
// Both modes compose to the same size; keeping the larger is only a guard
|
||||||
|
// against a fallback name caption widening one of them.
|
||||||
|
result.size = result.size.expandedTo(getLogicalSize(face));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
||||||
|
const std::string& iconDir,
|
||||||
|
ItemIconCache* itemIcons, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_sim(sim)
|
||||||
|
, m_config(config)
|
||||||
|
, m_iconDir(iconDir)
|
||||||
|
, m_itemIcons(itemIcons)
|
||||||
|
{
|
||||||
|
// The bar floats over the rendered world rather than sitting in a panel, so it
|
||||||
|
// brings its own opaque background to stay legible over any world content
|
||||||
|
// (REQ-UI-BUILD-BAR). Palette colors keep it consistent with the buttons it holds
|
||||||
|
// and with the side panels; this is widget chrome, not world rendering, so it is
|
||||||
|
// deliberately not a visuals.toml color.
|
||||||
|
setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
setStyleSheet(QStringLiteral(
|
||||||
|
"BuildButtonBar { background-color: palette(window);"
|
||||||
|
" border: 1px solid palette(mid); border-radius: 4px; }"));
|
||||||
|
|
||||||
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
|
layout->setSpacing(4);
|
||||||
|
layout->setContentsMargins(6, 4, 6, 4);
|
||||||
|
|
||||||
|
QSignalMapper* mapper = new QSignalMapper(this);
|
||||||
|
|
||||||
|
// Block icon shown to the right of each button's cost (REQ-UI-BUILD-COST); null
|
||||||
|
// when no building_block icon exists, in which case the cost is the bare number.
|
||||||
|
const QPixmap blockIcon = m_itemIcons->hasIcon(kBlockItemId)
|
||||||
|
? m_itemIcons->getPixmap(kBlockItemId, QFontMetrics(font()).height())
|
||||||
|
: QPixmap();
|
||||||
|
|
||||||
|
for (const BuildingDef& def : config->buildings.buildings)
|
||||||
|
{
|
||||||
|
if (!def.playerPlaceable)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Tunnel Entry and Tunnel Exit share a single "Tunnel" button; the exit is
|
||||||
|
// reached through the unified tunnel build mode, not its own button
|
||||||
|
// (REQ-BLD-TUNNEL-MODE, REQ-UI-BUILD-BAR). Both stay player-placeable so
|
||||||
|
// blueprints and cost totals still account for exits.
|
||||||
|
if (def.type == BuildingType::TunnelExit)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_types.push_back(def.type);
|
||||||
|
m_costs[def.type] = def.cost;
|
||||||
|
|
||||||
|
const QString name = (def.type == BuildingType::TunnelEntry)
|
||||||
|
? tr("Tunnel")
|
||||||
|
: QString::fromStdString(toDisplayName(def.id));
|
||||||
|
// Icon file name matches the building id (REQ-UI-BUILD-ICON); Tunnel Entry's
|
||||||
|
// "tunnel_entry.svg" serves the shared Tunnel button.
|
||||||
|
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
|
||||||
|
+ QString::fromStdString(def.id) + ".svg";
|
||||||
|
|
||||||
|
const ButtonFace face = buildButtonFace(
|
||||||
|
loadChipPixmaps(iconPath), InputMapper::getBuildHotkeyLabel(def.type), name,
|
||||||
|
QString::number(def.cost), blockIcon, font(), palette());
|
||||||
|
|
||||||
|
QPushButton* btn = new QPushButton(this);
|
||||||
|
btn->setIcon(face.icon);
|
||||||
|
btn->setIconSize(face.size);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
// The button face carries no name (REQ-UI-BUILD-COST), so the tooltip always
|
||||||
|
// leads with it and adds the config description when there is one
|
||||||
|
// (REQ-UI-BUILD-TOOLTIP).
|
||||||
|
btn->setToolTip(def.tooltip
|
||||||
|
? QStringLiteral("%1\n%2").arg(name, QString::fromStdString(*def.tooltip))
|
||||||
|
: name);
|
||||||
|
layout->addWidget(btn);
|
||||||
|
|
||||||
|
const int idx = static_cast<int>(m_buttons.size());
|
||||||
|
m_buttons.push_back(btn);
|
||||||
|
mapper->setMapping(btn, idx);
|
||||||
|
connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));
|
||||||
|
}
|
||||||
|
connect(mapper, qOverload<int>(&QSignalMapper::mapped), this, &BuildButtonBar::onBuildButton);
|
||||||
|
|
||||||
|
// Set apart from the building-type buttons by a gap, because it toggles a mode
|
||||||
|
// rather than selecting a building type (REQ-UI-DECONSTRUCT-BUTTON). A fixed
|
||||||
|
// spacer rather than a stretch: the bar is sized to its contents, so there is no
|
||||||
|
// right edge for a stretch to push against.
|
||||||
|
layout->addSpacing(kDeconstructGapPx);
|
||||||
|
|
||||||
|
// Having no cost, it shows its name where the building buttons show theirs
|
||||||
|
// (REQ-UI-DECONSTRUCT-BUTTON), and its Q toggle as the badge (REQ-UI-HOTKEYS).
|
||||||
|
const ButtonFace deconstructFace = buildButtonFace(
|
||||||
|
loadChipPixmaps(QString::fromStdString(m_iconDir) + "/deconstruct.svg"),
|
||||||
|
QStringLiteral("Q"), tr("Deconstruct"), tr("Deconstruct"), QPixmap(),
|
||||||
|
font(), palette());
|
||||||
|
|
||||||
|
m_deconstructButton = new QPushButton(this);
|
||||||
|
m_deconstructButton->setCheckable(true);
|
||||||
|
m_deconstructButton->setIcon(deconstructFace.icon);
|
||||||
|
m_deconstructButton->setIconSize(deconstructFace.size);
|
||||||
|
// Refund tooltip composed from world.refund_percentage (REQ-UI-DECONSTRUCT-BUTTON,
|
||||||
|
// REQ-BLD-DECONSTRUCT). A finished building refunds the configured percentage; a
|
||||||
|
// construction site removed before it is built is refunded in full. When the
|
||||||
|
// percentage is 100% both cases coincide, so the tooltip is simplified to one case.
|
||||||
|
const int refundPercentage = m_config->world.refundPercentage;
|
||||||
|
const QString deconstructTooltip = (refundPercentage >= 100)
|
||||||
|
? tr("Deconstruct buildings. Refunds %1% of the building block cost.")
|
||||||
|
.arg(refundPercentage)
|
||||||
|
: tr("Deconstruct buildings. A finished building refunds %1% of its building "
|
||||||
|
"block cost once removed; a construction site removed before it is built "
|
||||||
|
"is refunded in full.")
|
||||||
|
.arg(refundPercentage);
|
||||||
|
m_deconstructButton->setToolTip(deconstructTooltip);
|
||||||
|
layout->addWidget(m_deconstructButton);
|
||||||
|
connect(m_deconstructButton, &QPushButton::clicked, this, [this]() {
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<DeconstructModeToggleRequestedEvent>());
|
||||||
|
});
|
||||||
|
|
||||||
|
updateVisibility();
|
||||||
|
registerForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildButtonBar::~BuildButtonBar()
|
||||||
|
{
|
||||||
|
unregisterForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::anchorTo(const QRect& worldViewRect)
|
||||||
|
{
|
||||||
|
m_viewRect = worldViewRect;
|
||||||
|
recenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::clearActiveButton()
|
||||||
|
{
|
||||||
|
if (m_activeIndex)
|
||||||
|
{
|
||||||
|
m_buttons[*m_activeIndex]->setChecked(false);
|
||||||
|
}
|
||||||
|
m_activeIndex.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::updateAffordability()
|
||||||
|
{
|
||||||
|
const int buildingBlocks = m_sim->getBuildingBlocksStock();
|
||||||
|
|
||||||
|
// If the currently selected tool can no longer be afforded, exit builder mode
|
||||||
|
// before recomputing button states so it does not stay selected. Clearing the
|
||||||
|
// active index first lets the loop below disable the now-unaffordable button.
|
||||||
|
if (m_activeIndex)
|
||||||
|
{
|
||||||
|
const BuildingType activeType = m_types[*m_activeIndex];
|
||||||
|
const std::map<BuildingType, int>::const_iterator it = m_costs.find(activeType);
|
||||||
|
const int cost = (it != m_costs.end()) ? it->second : 0;
|
||||||
|
if (buildingBlocks < cost)
|
||||||
|
{
|
||||||
|
clearActiveButton();
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ExitBuilderModeRequestedEvent>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < m_buttons.size(); ++i)
|
||||||
|
{
|
||||||
|
const BuildingType type = m_types[i];
|
||||||
|
const std::map<BuildingType, int>::const_iterator it = m_costs.find(type);
|
||||||
|
const int cost = (it != m_costs.end()) ? it->second : 0;
|
||||||
|
m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::updateVisibility()
|
||||||
|
{
|
||||||
|
// A locked building type's button is hidden until its unlock group is awarded
|
||||||
|
// (REQ-LOCK-BUILDING). Buttons keep their index so hotkeys/affordability stay
|
||||||
|
// stable; they simply appear once the type is unlocked.
|
||||||
|
for (std::size_t i = 0; i < m_buttons.size(); ++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
|
||||||
|
// re-center on it (REQ-UI-BUILD-BAR).
|
||||||
|
recenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_buttons.size()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::size_t idx = static_cast<std::size_t>(index);
|
||||||
|
|
||||||
|
if (m_activeIndex == idx)
|
||||||
|
{
|
||||||
|
clearActiveButton();
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ExitBuilderModeRequestedEvent>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_activeIndex)
|
||||||
|
{
|
||||||
|
m_buttons[*m_activeIndex]->setChecked(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_activeIndex = idx;
|
||||||
|
m_buttons[idx]->setChecked(true);
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BuildingTypeSelectedEvent>(m_types[idx]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
clearActiveButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
updateAffordability();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::handleEvent(std::shared_ptr<const UnlockedBuildingsChangedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
updateVisibility();
|
||||||
|
updateAffordability();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event)
|
||||||
|
{
|
||||||
|
m_deconstructButton->setChecked(event->active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonBar::handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < m_types.size(); ++i)
|
||||||
|
{
|
||||||
|
if (m_types[i] == event->type)
|
||||||
|
{
|
||||||
|
// Equivalent to clicking the build button: a disabled (unaffordable) or
|
||||||
|
// hidden (locked, REQ-LOCK-BUILDING) button cannot be clicked, so the
|
||||||
|
// hotkey is likewise inert.
|
||||||
|
if (m_buttons[i]->isEnabled() && m_buttons[i]->isVisible())
|
||||||
|
{
|
||||||
|
onBuildButton(static_cast<int>(i));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "BuilderModeExitedEvent.h"
|
#include "BuilderModeExitedEvent.h"
|
||||||
@@ -21,25 +22,34 @@ class QPushButton;
|
|||||||
class Simulation;
|
class Simulation;
|
||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
|
|
||||||
class BuildButtonGrid : public QWidget,
|
// The build menu: one horizontal row of build buttons floating over the game world
|
||||||
public CombinedEventHandler<BuilderModeExitedEvent,
|
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons; its owner hands it the
|
||||||
DeconstructModeChangedEvent,
|
// world view's rect through anchorTo() and it centers itself along that rect's
|
||||||
BuildHotkeyPressedEvent,
|
// bottom edge.
|
||||||
BuildingBlocksChangedEvent,
|
class BuildButtonBar : public QWidget,
|
||||||
UnlockedBuildingsChangedEvent>
|
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
|
DeconstructModeChangedEvent,
|
||||||
|
BuildHotkeyPressedEvent,
|
||||||
|
BuildingBlocksChangedEvent,
|
||||||
|
UnlockedBuildingsChangedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// iconDir is the directory holding the per-building "<id>.svg" chip icons
|
// iconDir is the directory holding the per-building "<id>.svg" chip icons
|
||||||
// (REQ-UI-BUILD-GRID), read from disk at runtime like the config files.
|
// (REQ-UI-BUILD-ICON), read from disk at runtime like the config files.
|
||||||
// itemIcons is the window-wide per-item icon cache and supplies the
|
// itemIcons is the window-wide per-item icon cache and supplies the
|
||||||
// building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Not
|
// building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Not
|
||||||
// owned; must outlive this widget.
|
// owned; must outlive this widget.
|
||||||
BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
BuildButtonBar(Simulation* sim, const GameConfig* config,
|
||||||
const std::string& iconDir, ItemIconCache* itemIcons,
|
const std::string& iconDir, ItemIconCache* itemIcons,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~BuildButtonGrid() override;
|
~BuildButtonBar() override;
|
||||||
|
|
||||||
|
// Centers the bar along the bottom edge of the game world view's rect, given in
|
||||||
|
// the bar's parent coordinates (REQ-UI-BUILD-BAR). The rect is remembered, so a
|
||||||
|
// re-center later driven by an unlock needs no second call from the owner.
|
||||||
|
void anchorTo(const QRect& worldViewRect);
|
||||||
|
|
||||||
void clearActiveButton();
|
void clearActiveButton();
|
||||||
|
|
||||||
@@ -53,6 +63,11 @@ 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;
|
||||||
@@ -72,4 +87,5 @@ 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;
|
||||||
};
|
};
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
#include "BuildButtonGrid.h"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QColor>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QFontMetrics>
|
|
||||||
#include <QGridLayout>
|
|
||||||
#include <QIcon>
|
|
||||||
#include <QPainter>
|
|
||||||
#include <QPaintEvent>
|
|
||||||
#include <QPalette>
|
|
||||||
#include <QPixmap>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QRect>
|
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QSignalMapper>
|
|
||||||
#include <QSize>
|
|
||||||
#include <QString>
|
|
||||||
#include <QSvgRenderer>
|
|
||||||
|
|
||||||
#include "BuildingType.h"
|
|
||||||
#include "BuildingTypeSelectedEvent.h"
|
|
||||||
#include "DeconstructModeToggleRequestedEvent.h"
|
|
||||||
#include "DisplayName.h"
|
|
||||||
#include "EventManager.h"
|
|
||||||
#include "ExitBuilderModeRequestedEvent.h"
|
|
||||||
#include "ItemIconCache.h"
|
|
||||||
#include "Simulation.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// Pixel size the SVG chips are rasterized at; downscaled to the button icon size.
|
|
||||||
const int kIconRenderSize = 64;
|
|
||||||
const QSize kIconSize(28, 28);
|
|
||||||
|
|
||||||
QPixmap renderChip(const QByteArray& svg)
|
|
||||||
{
|
|
||||||
QSvgRenderer renderer(svg);
|
|
||||||
QPixmap pixmap(kIconRenderSize, kIconRenderSize);
|
|
||||||
pixmap.fill(Qt::transparent);
|
|
||||||
QPainter painter(&pixmap);
|
|
||||||
renderer.render(&painter);
|
|
||||||
return pixmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds a build-button icon from a "<id>.svg" chip file. The returned QIcon also
|
|
||||||
// carries a Disabled-mode pixmap whose chip background is recolored grey, so an
|
|
||||||
// unaffordable (disabled) button shows the grey variant automatically
|
|
||||||
// (REQ-UI-BUILD-DISABLED) without any extra work in updateAffordability().
|
|
||||||
QIcon loadBuildingIcon(const QString& path)
|
|
||||||
{
|
|
||||||
QFile file(path);
|
|
||||||
if (!file.open(QIODevice::ReadOnly))
|
|
||||||
{
|
|
||||||
return QIcon();
|
|
||||||
}
|
|
||||||
const QByteArray svg = file.readAll();
|
|
||||||
|
|
||||||
QIcon icon;
|
|
||||||
icon.addPixmap(renderChip(svg), QIcon::Normal);
|
|
||||||
|
|
||||||
// Recolor only the chip background: the first "#rrggbb" fill in the file is the
|
|
||||||
// rounded background rect; the white glyph uses fill="none" and is left alone.
|
|
||||||
QString greyed = QString::fromUtf8(svg);
|
|
||||||
static const QRegularExpression fillPattern(QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
|
|
||||||
const QRegularExpressionMatch match = fillPattern.match(greyed);
|
|
||||||
if (match.hasMatch())
|
|
||||||
{
|
|
||||||
greyed.replace(match.capturedStart(), match.capturedLength(),
|
|
||||||
QStringLiteral("fill=\"#5f636e\""));
|
|
||||||
}
|
|
||||||
icon.addPixmap(renderChip(greyed.toUtf8()), QIcon::Disabled);
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal and grey-background chip pixmaps for a "<id>.svg" file, using the same
|
|
||||||
// recolor rule as loadBuildingIcon. Empty pixmaps if the file cannot be read.
|
|
||||||
struct ChipPixmaps { QPixmap normal; QPixmap grey; };
|
|
||||||
ChipPixmaps loadChipPixmaps(const QString& path)
|
|
||||||
{
|
|
||||||
QFile file(path);
|
|
||||||
if (!file.open(QIODevice::ReadOnly)) { return {}; }
|
|
||||||
const QByteArray svg = file.readAll();
|
|
||||||
|
|
||||||
ChipPixmaps result;
|
|
||||||
result.normal = renderChip(svg);
|
|
||||||
QString greyed = QString::fromUtf8(svg);
|
|
||||||
static const QRegularExpression fillPattern(QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
|
|
||||||
const QRegularExpressionMatch match = fillPattern.match(greyed);
|
|
||||||
if (match.hasMatch())
|
|
||||||
{
|
|
||||||
greyed.replace(match.capturedStart(), match.capturedLength(),
|
|
||||||
QStringLiteral("fill=\"#5f636e\""));
|
|
||||||
}
|
|
||||||
result.grey = renderChip(greyed.toUtf8());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A build button that paints its own face — chip icon at the left, the building
|
|
||||||
// name above the cost, and the building_block item icon after the cost number in
|
|
||||||
// place of the "Blocks" word (REQ-UI-BUILD-COST, REQ-UI-BUILD-ICON). Custom paint
|
|
||||||
// (rather than the native icon+text) is needed because a QPushButton holds only
|
|
||||||
// one icon; this stays adaptive to the button width and greys itself when the
|
|
||||||
// button is disabled/unaffordable (REQ-UI-BUILD-DISABLED).
|
|
||||||
class BuildButton : public QPushButton
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
BuildButton(const ChipPixmaps& chip, const QString& name,
|
|
||||||
const QString& costText, const QPixmap& blockIcon, QWidget* parent)
|
|
||||||
: QPushButton(parent)
|
|
||||||
, m_chip(chip)
|
|
||||||
, m_name(name)
|
|
||||||
, m_costText(costText)
|
|
||||||
, m_blockIcon(blockIcon)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void paintEvent(QPaintEvent* event) override
|
|
||||||
{
|
|
||||||
QPushButton::paintEvent(event); // frame, checked/hover state
|
|
||||||
|
|
||||||
QPainter painter(this);
|
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
||||||
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
|
||||||
|
|
||||||
const bool on = isEnabled();
|
|
||||||
const QRect area = rect().adjusted(6, 4, -6, -4);
|
|
||||||
const int chipSize = kIconSize.width();
|
|
||||||
const QPixmap& chip = on ? m_chip.normal : m_chip.grey;
|
|
||||||
if (!chip.isNull())
|
|
||||||
{
|
|
||||||
painter.drawPixmap(
|
|
||||||
QRect(area.x(), area.y() + (area.height() - chipSize) / 2,
|
|
||||||
chipSize, chipSize), chip);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QRect textArea(area.x() + chipSize + 6, area.y(),
|
|
||||||
area.width() - chipSize - 6, area.height());
|
|
||||||
const QFontMetrics metrics(font());
|
|
||||||
const int lineHeight = metrics.height();
|
|
||||||
painter.setFont(font());
|
|
||||||
painter.setPen(palette().color(on ? QPalette::Active : QPalette::Disabled,
|
|
||||||
QPalette::ButtonText));
|
|
||||||
|
|
||||||
// Name fills everything above the bottom cost line (word-wrapped).
|
|
||||||
painter.drawText(
|
|
||||||
QRect(textArea.x(), textArea.y(),
|
|
||||||
textArea.width(), textArea.height() - lineHeight),
|
|
||||||
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_name);
|
|
||||||
|
|
||||||
// Cost line: "<n>" then the block icon (or "<n> Blocks" when no icon).
|
|
||||||
const int costY = textArea.bottom() - lineHeight + 1;
|
|
||||||
if (m_blockIcon.isNull())
|
|
||||||
{
|
|
||||||
painter.drawText(
|
|
||||||
QRect(textArea.x(), costY, textArea.width(), lineHeight),
|
|
||||||
Qt::AlignLeft | Qt::AlignVCenter,
|
|
||||||
QObject::tr("%1 Blocks").arg(m_costText));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const int costWidth = metrics.horizontalAdvance(m_costText);
|
|
||||||
painter.drawText(QRect(textArea.x(), costY, costWidth, lineHeight),
|
|
||||||
Qt::AlignLeft | Qt::AlignVCenter, m_costText);
|
|
||||||
const qreal iconDpr = m_blockIcon.devicePixelRatio();
|
|
||||||
const int iconW = static_cast<int>(m_blockIcon.width() / iconDpr);
|
|
||||||
const int iconH = static_cast<int>(m_blockIcon.height() / iconDpr);
|
|
||||||
if (!on) { painter.setOpacity(0.45); }
|
|
||||||
painter.drawPixmap(
|
|
||||||
QRect(textArea.x() + costWidth + 4, costY + (lineHeight - iconH) / 2,
|
|
||||||
iconW, iconH), m_blockIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
ChipPixmaps m_chip;
|
|
||||||
QString m_name;
|
|
||||||
QString m_costText;
|
|
||||||
QPixmap m_blockIcon;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
|
||||||
const std::string& iconDir,
|
|
||||||
ItemIconCache* itemIcons, QWidget* parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
, m_sim(sim)
|
|
||||||
, m_config(config)
|
|
||||||
, m_iconDir(iconDir)
|
|
||||||
, m_itemIcons(itemIcons)
|
|
||||||
{
|
|
||||||
QGridLayout* layout = new QGridLayout(this);
|
|
||||||
layout->setSpacing(4);
|
|
||||||
layout->setContentsMargins(4, 4, 4, 4);
|
|
||||||
|
|
||||||
QSignalMapper* mapper = new QSignalMapper(this);
|
|
||||||
int col = 0;
|
|
||||||
int row = 0;
|
|
||||||
const int kCols = 3;
|
|
||||||
|
|
||||||
// Block icon shown in each button's cost line (REQ-UI-BUILD-COST); null when no
|
|
||||||
// building_block icon exists, in which case buttons fall back to text costs.
|
|
||||||
const bool hasBlockIcon = m_itemIcons->hasIcon("building_block");
|
|
||||||
const QPixmap blockIcon = hasBlockIcon
|
|
||||||
? m_itemIcons->getPixmap("building_block", QFontMetrics(font()).height())
|
|
||||||
: QPixmap();
|
|
||||||
|
|
||||||
for (const BuildingDef& def : config->buildings.buildings)
|
|
||||||
{
|
|
||||||
if (!def.playerPlaceable)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Tunnel Entry and Tunnel Exit share a single "Tunnel" button; the exit is
|
|
||||||
// reached through the unified tunnel build mode, not its own button
|
|
||||||
// (REQ-BLD-TUNNEL-MODE, REQ-UI-BUILD-GRID). Both stay player-placeable so
|
|
||||||
// blueprints and cost totals still account for exits.
|
|
||||||
if (def.type == BuildingType::TunnelExit)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
m_types.push_back(def.type);
|
|
||||||
m_costs[def.type] = def.cost;
|
|
||||||
|
|
||||||
const QString name = (def.type == BuildingType::TunnelEntry)
|
|
||||||
? tr("Tunnel")
|
|
||||||
: QString::fromStdString(toDisplayName(def.id));
|
|
||||||
// Icon file name matches the building id (REQ-UI-BUILD-GRID); Tunnel Entry's
|
|
||||||
// "tunnel_entry.svg" serves the shared Tunnel button.
|
|
||||||
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
|
|
||||||
+ QString::fromStdString(def.id) + ".svg";
|
|
||||||
|
|
||||||
QPushButton* btn = nullptr;
|
|
||||||
if (hasBlockIcon)
|
|
||||||
{
|
|
||||||
// Custom-painted button showing the cost with the block icon in place of
|
|
||||||
// the "Blocks" word (REQ-UI-BUILD-COST).
|
|
||||||
btn = new BuildButton(loadChipPixmaps(iconPath), name,
|
|
||||||
QString::number(def.cost), blockIcon, this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Fallback: native chip icon + text cost when no block icon exists.
|
|
||||||
btn = new QPushButton(name + "\n" + tr("%1 Blocks").arg(def.cost), this);
|
|
||||||
btn->setIcon(loadBuildingIcon(iconPath));
|
|
||||||
btn->setIconSize(kIconSize);
|
|
||||||
}
|
|
||||||
btn->setCheckable(true);
|
|
||||||
btn->setFixedHeight(48);
|
|
||||||
if (def.tooltip)
|
|
||||||
{
|
|
||||||
btn->setToolTip(QString::fromStdString(*def.tooltip));
|
|
||||||
}
|
|
||||||
layout->addWidget(btn, row, col);
|
|
||||||
|
|
||||||
const int idx = static_cast<int>(m_buttons.size());
|
|
||||||
m_buttons.push_back(btn);
|
|
||||||
mapper->setMapping(btn, idx);
|
|
||||||
connect(btn, &QPushButton::clicked, mapper, qOverload<>(&QSignalMapper::map));
|
|
||||||
|
|
||||||
++col;
|
|
||||||
if (col >= kCols)
|
|
||||||
{
|
|
||||||
col = 0;
|
|
||||||
++row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
connect(mapper, qOverload<int>(&QSignalMapper::mapped), this, &BuildButtonGrid::onBuildButton);
|
|
||||||
|
|
||||||
m_deconstructButton = new QPushButton(tr("Deconstruct"), this);
|
|
||||||
m_deconstructButton->setCheckable(true);
|
|
||||||
m_deconstructButton->setFixedHeight(48);
|
|
||||||
m_deconstructButton->setIcon(loadBuildingIcon(
|
|
||||||
QString::fromStdString(m_iconDir) + "/deconstruct.svg"));
|
|
||||||
m_deconstructButton->setIconSize(kIconSize);
|
|
||||||
// Refund tooltip composed from world.refund_percentage (REQ-UI-DECONSTRUCT-BUTTON,
|
|
||||||
// REQ-BLD-DECONSTRUCT). A finished building refunds the configured percentage; a
|
|
||||||
// construction site removed before it is built is refunded in full. When the
|
|
||||||
// percentage is 100% both cases coincide, so the tooltip is simplified to one case.
|
|
||||||
const int refundPercentage = m_config->world.refundPercentage;
|
|
||||||
const QString deconstructTooltip = (refundPercentage >= 100)
|
|
||||||
? tr("Deconstruct buildings. Refunds %1% of the building block cost.")
|
|
||||||
.arg(refundPercentage)
|
|
||||||
: tr("Deconstruct buildings. A finished building refunds %1% of its building "
|
|
||||||
"block cost once removed; a construction site removed before it is built "
|
|
||||||
"is refunded in full.")
|
|
||||||
.arg(refundPercentage);
|
|
||||||
m_deconstructButton->setToolTip(deconstructTooltip);
|
|
||||||
layout->addWidget(m_deconstructButton, row, col);
|
|
||||||
connect(m_deconstructButton, &QPushButton::clicked, this, [this]() {
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<DeconstructModeToggleRequestedEvent>());
|
|
||||||
});
|
|
||||||
|
|
||||||
updateVisibility();
|
|
||||||
registerForEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildButtonGrid::~BuildButtonGrid()
|
|
||||||
{
|
|
||||||
unregisterForEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::updateAffordability()
|
|
||||||
{
|
|
||||||
const int buildingBlocks = m_sim->getBuildingBlocksStock();
|
|
||||||
|
|
||||||
// If the currently selected tool can no longer be afforded, exit builder mode
|
|
||||||
// before recomputing button states so it does not stay selected. Clearing the
|
|
||||||
// active index first lets the loop below disable the now-unaffordable button.
|
|
||||||
if (m_activeIndex)
|
|
||||||
{
|
|
||||||
const BuildingType activeType = m_types[*m_activeIndex];
|
|
||||||
const std::map<BuildingType, int>::const_iterator it = m_costs.find(activeType);
|
|
||||||
const int cost = (it != m_costs.end()) ? it->second : 0;
|
|
||||||
if (buildingBlocks < cost)
|
|
||||||
{
|
|
||||||
clearActiveButton();
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<ExitBuilderModeRequestedEvent>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < m_buttons.size(); ++i)
|
|
||||||
{
|
|
||||||
const BuildingType type = m_types[i];
|
|
||||||
const std::map<BuildingType, int>::const_iterator it = m_costs.find(type);
|
|
||||||
const int cost = (it != m_costs.end()) ? it->second : 0;
|
|
||||||
m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::updateVisibility()
|
|
||||||
{
|
|
||||||
// A locked building type's button is hidden until its unlock group is awarded
|
|
||||||
// (REQ-LOCK-BUILDING). Buttons keep their index so hotkeys/affordability stay
|
|
||||||
// stable; they simply appear once the type is unlocked.
|
|
||||||
for (std::size_t i = 0; i < m_buttons.size(); ++i)
|
|
||||||
{
|
|
||||||
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::clearActiveButton()
|
|
||||||
{
|
|
||||||
if (m_activeIndex)
|
|
||||||
{
|
|
||||||
m_buttons[*m_activeIndex]->setChecked(false);
|
|
||||||
}
|
|
||||||
m_activeIndex.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::onBuildButton(int index)
|
|
||||||
{
|
|
||||||
if (index < 0 || index >= static_cast<int>(m_buttons.size()))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::size_t idx = static_cast<std::size_t>(index);
|
|
||||||
|
|
||||||
if (m_activeIndex == idx)
|
|
||||||
{
|
|
||||||
clearActiveButton();
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<ExitBuilderModeRequestedEvent>());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_activeIndex)
|
|
||||||
{
|
|
||||||
m_buttons[*m_activeIndex]->setChecked(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_activeIndex = idx;
|
|
||||||
m_buttons[idx]->setChecked(true);
|
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
|
||||||
std::make_shared<BuildingTypeSelectedEvent>(m_types[idx]));
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
clearActiveButton();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
updateAffordability();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::handleEvent(std::shared_ptr<const UnlockedBuildingsChangedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
updateVisibility();
|
|
||||||
updateAffordability();
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event)
|
|
||||||
{
|
|
||||||
m_deconstructButton->setChecked(event->active);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event)
|
|
||||||
{
|
|
||||||
for (std::size_t i = 0; i < m_types.size(); ++i)
|
|
||||||
{
|
|
||||||
if (m_types[i] == event->type)
|
|
||||||
{
|
|
||||||
// Equivalent to clicking the build button: a disabled (unaffordable) or
|
|
||||||
// hidden (locked, REQ-LOCK-BUILDING) button cannot be clicked, so the
|
|
||||||
// hotkey is likewise inert.
|
|
||||||
if (m_buttons[i]->isEnabled() && m_buttons[i]->isVisible())
|
|
||||||
{
|
|
||||||
onBuildButton(static_cast<int>(i));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ 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}/BuildButtonGrid.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
||||||
@@ -35,7 +35,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ void GameWorldView::onFrame()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unlocked building set changes only after a drop is applied or on Restart
|
// Unlocked building set changes only after a drop is applied or on Restart
|
||||||
// (REQ-LOCK-BUILDING); the build grid rebuilds its visible buttons.
|
// (REQ-LOCK-BUILDING); the build bar rebuilds its visible buttons.
|
||||||
int newUnlockedBuildingCount = 0;
|
int newUnlockedBuildingCount = 0;
|
||||||
for (const BuildingDef& def : m_sim->getConfig().buildings.buildings)
|
for (const BuildingDef& def : m_sim->getConfig().buildings.buildings)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,13 +21,6 @@
|
|||||||
#include "SpeedChangeRequestedEvent.h"
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// Item id of the building blocks resource, whose icon stands in for the "Blocks"
|
|
||||||
// word in the header stock and expand button (REQ-UI-BLOCKS-ICON).
|
|
||||||
const char* const kBlockItemId = "building_block";
|
|
||||||
}
|
|
||||||
|
|
||||||
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||||
const int HeaderBar::kSpeedCount = 5;
|
const int HeaderBar::kSpeedCount = 5;
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,9 @@
|
|||||||
// theme-correct; the result is devicePixelRatio-aware so text and icon stay crisp.
|
// theme-correct; the result is devicePixelRatio-aware so text and icon stay crisp.
|
||||||
QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
||||||
const QFont& font, const QColor& textColor);
|
const QFont& font, const QColor& textColor);
|
||||||
|
|
||||||
|
// Item id of the building blocks resource, whose icon stands in for the "Blocks"
|
||||||
|
// word wherever a cost or stock is captioned (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST,
|
||||||
|
// REQ-UI-EXPAND-BUTTON). It lives next to the caption helper because every caller of
|
||||||
|
// one is a caller of the other.
|
||||||
|
const char* const kBlockItemId = "building_block";
|
||||||
|
|||||||
@@ -51,6 +51,27 @@ std::optional<BuildingType> buildHotkeyType(int digit, bool shiftHeld)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
QString InputMapper::getBuildHotkeyLabel(BuildingType type)
|
||||||
|
{
|
||||||
|
// Searched out of the binding table above rather than spelled out a second time,
|
||||||
|
// so a badge can never claim a key the handler does not act on. The shift glyph
|
||||||
|
// is written as a code point because the sources are not guaranteed to be read
|
||||||
|
// as UTF-8 by every compiler this builds with.
|
||||||
|
const QChar shiftGlyph(0x21E7); // U+21E7 UPWARDS WHITE ARROW
|
||||||
|
for (int digit = 1; digit <= 9; ++digit)
|
||||||
|
{
|
||||||
|
if (buildHotkeyType(digit, false) == type)
|
||||||
|
{
|
||||||
|
return QString::number(digit);
|
||||||
|
}
|
||||||
|
if (buildHotkeyType(digit, true) == type)
|
||||||
|
{
|
||||||
|
return shiftGlyph + QString::number(digit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
bool InputMapper::handleKeyPress(QKeyEvent* event)
|
bool InputMapper::handleKeyPress(QKeyEvent* event)
|
||||||
{
|
{
|
||||||
// Auto-repeat says nothing new about which keys are down, and a held action is
|
// Auto-repeat says nothing new about which keys are down, and a held action is
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
#include "WorldCamera.h"
|
#include "WorldCamera.h"
|
||||||
|
|
||||||
class QKeyEvent;
|
class QKeyEvent;
|
||||||
@@ -24,6 +27,12 @@ class QKeyEvent;
|
|||||||
class InputMapper
|
class InputMapper
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// The build hotkey that selects the given building type, spelled for display on
|
||||||
|
// its build button's badge (REQ-UI-BUILD-COST): "1" for a plain digit, "⇧1" for
|
||||||
|
// a Shift+digit, and an empty string for a building type with no build hotkey.
|
||||||
|
// Lives here so the badge and the key handling read the same binding table.
|
||||||
|
static QString getBuildHotkeyLabel(BuildingType type);
|
||||||
|
|
||||||
// Both return true when the key was consumed; the caller passes anything else
|
// Both return true when the key was consumed; the caller passes anything else
|
||||||
// on to its base class so unrelated shortcuts keep working.
|
// on to its base class so unrelated shortcuts keep working.
|
||||||
bool handleKeyPress(QKeyEvent* event);
|
bool handleKeyPress(QKeyEvent* event);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "BlueprintPanel.h"
|
#include "BlueprintPanel.h"
|
||||||
#include "BuildButtonGrid.h"
|
#include "BuildButtonBar.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "CommandRequestedEvent.h"
|
#include "CommandRequestedEvent.h"
|
||||||
@@ -58,36 +58,43 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
||||||
m_itemIcons.get(), m_replay.get(), this);
|
m_itemIcons.get(), m_replay.get(), this);
|
||||||
|
|
||||||
m_sidePanel = new QWidget(this);
|
|
||||||
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
|
||||||
sideLayout->setContentsMargins(1, 1, 1, 1);
|
|
||||||
sideLayout->setSpacing(1);
|
|
||||||
|
|
||||||
// Building icons live alongside the config (a sibling of the config dir), read
|
// Building icons live alongside the config (a sibling of the config dir), read
|
||||||
// from disk at runtime the same way visuals.toml is.
|
// from disk at runtime the same way visuals.toml is.
|
||||||
const std::string iconDir = QDir::cleanPath(
|
const std::string iconDir = QDir::cleanPath(
|
||||||
QString::fromStdString(m_configDir) + "/../icons/buildings").toStdString();
|
QString::fromStdString(m_configDir) + "/../icons/buildings").toStdString();
|
||||||
|
|
||||||
|
// Floats over the game world rather than living in the side panel column
|
||||||
|
// (REQ-UI-BUILD-BAR). Creation order is the stacking order for siblings, so
|
||||||
|
// building it after the world view puts it above the world and its vignettes,
|
||||||
|
// and before the dim overlay keeps modals dimming it too (REQ-UI-MODAL-DIM).
|
||||||
|
// Its geometry comes from layoutPanels().
|
||||||
|
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(), iconDir,
|
||||||
|
m_itemIcons.get(), this);
|
||||||
|
|
||||||
|
m_sidePanel = new QWidget(this);
|
||||||
|
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||||
|
sideLayout->setContentsMargins(1, 1, 1, 1);
|
||||||
|
sideLayout->setSpacing(1);
|
||||||
|
|
||||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||||
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), iconDir, m_itemIcons.get(), m_sidePanel);
|
|
||||||
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
|
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||||
|
|
||||||
|
// Equal stretch gives the two panels half the column height each
|
||||||
|
// (REQ-UI-PANEL-COLUMN).
|
||||||
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||||
sideLayout->addWidget(m_buildButtonGrid, 1);
|
|
||||||
sideLayout->addWidget(m_blueprintPanel, 1);
|
sideLayout->addWidget(m_blueprintPanel, 1);
|
||||||
|
|
||||||
// Draw a thin border around each of the three side-panel sections. The class
|
// Draw a thin border around each of the two side-panel sections. The class
|
||||||
// scoped selectors keep the border on the panels themselves rather than
|
// scoped selectors keep the border on the panels themselves rather than
|
||||||
// cascading onto their child widgets; WA_StyledBackground lets the plain
|
// cascading onto their child widgets; WA_StyledBackground lets the plain
|
||||||
// QWidget subclasses honor the stylesheet box (border/background).
|
// QWidget subclasses honor the stylesheet box (border/background).
|
||||||
for (QWidget* panel : { static_cast<QWidget*>(m_selectedBuildingPanel),
|
for (QWidget* panel : { static_cast<QWidget*>(m_selectedBuildingPanel),
|
||||||
static_cast<QWidget*>(m_buildButtonGrid),
|
|
||||||
static_cast<QWidget*>(m_blueprintPanel) })
|
static_cast<QWidget*>(m_blueprintPanel) })
|
||||||
{
|
{
|
||||||
panel->setAttribute(Qt::WA_StyledBackground, true);
|
panel->setAttribute(Qt::WA_StyledBackground, true);
|
||||||
}
|
}
|
||||||
m_sidePanel->setStyleSheet(QStringLiteral(
|
m_sidePanel->setStyleSheet(QStringLiteral(
|
||||||
"SelectedBuildingPanel, BuildButtonGrid, BlueprintPanel {"
|
"SelectedBuildingPanel, BlueprintPanel {"
|
||||||
" border: 1px solid palette(mid); }"));
|
" border: 1px solid palette(mid); }"));
|
||||||
|
|
||||||
// Created last so it stacks above the other children; covers the whole window and
|
// Created last so it stacks above the other children; covers the whole window and
|
||||||
@@ -164,6 +171,9 @@ void MainWindow::layoutPanels()
|
|||||||
m_headerBar->setGeometry(0, 0, mainW, headerH);
|
m_headerBar->setGeometry(0, 0, mainW, headerH);
|
||||||
m_gameWorldView->setGeometry(0, headerH, mainW, totalH - headerH);
|
m_gameWorldView->setGeometry(0, headerH, mainW, totalH - headerH);
|
||||||
m_sidePanel->setGeometry(mainW, 0, sideW, totalH);
|
m_sidePanel->setGeometry(mainW, 0, sideW, totalH);
|
||||||
|
// Sizes itself to its buttons and centers along the bottom of the world view
|
||||||
|
// (REQ-UI-BUILD-BAR).
|
||||||
|
m_buildButtonBar->anchorTo(QRect(0, headerH, mainW, totalH - headerH));
|
||||||
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Simulation;
|
|||||||
class GameWorldView;
|
class GameWorldView;
|
||||||
class HeaderBar;
|
class HeaderBar;
|
||||||
class SelectedBuildingPanel;
|
class SelectedBuildingPanel;
|
||||||
class BuildButtonGrid;
|
class BuildButtonBar;
|
||||||
class BlueprintPanel;
|
class BlueprintPanel;
|
||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
class QCloseEvent;
|
class QCloseEvent;
|
||||||
@@ -78,12 +78,12 @@ private:
|
|||||||
VisualsConfig m_visuals;
|
VisualsConfig m_visuals;
|
||||||
Simulation* m_sim;
|
Simulation* m_sim;
|
||||||
// One per-item icon cache for the whole window (REQ-UI-ITEM-ICON): the header,
|
// One per-item icon cache for the whole window (REQ-UI-ITEM-ICON): the header,
|
||||||
// build grid, world view, and recipe dialog all rasterize the same SVGs.
|
// build bar, world view, and recipe dialog all rasterize the same SVGs.
|
||||||
std::unique_ptr<ItemIconCache> m_itemIcons;
|
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||||
GameWorldView* m_gameWorldView;
|
GameWorldView* m_gameWorldView;
|
||||||
HeaderBar* m_headerBar;
|
HeaderBar* m_headerBar;
|
||||||
SelectedBuildingPanel* m_selectedBuildingPanel;
|
SelectedBuildingPanel* m_selectedBuildingPanel;
|
||||||
BuildButtonGrid* m_buildButtonGrid;
|
BuildButtonBar* m_buildButtonBar;
|
||||||
BlueprintPanel* m_blueprintPanel;
|
BlueprintPanel* m_blueprintPanel;
|
||||||
QWidget* m_sidePanel;
|
QWidget* m_sidePanel;
|
||||||
ModalDimOverlay* m_dimOverlay = nullptr;
|
ModalDimOverlay* m_dimOverlay = nullptr;
|
||||||
|
|||||||
Reference in New Issue
Block a user