host the modals in the window instead of in windows of their own
Every dialog was an OS window with a dim widget behind it in the game window. That is why a click beside one could not reach it: Qt drops mouse events for a window a modal blocks, so the dim -- a child of the blocked window -- never saw the press. ModalLayer is that dim grown up: still a child of the main window covering its rect, but it now hosts the open modal, places it, and runs its event loop, so it is the widget the clicks beside a modal land on. It keeps a stack, paints one dim however many modals are open, and a hold keeps it up while one modal hands over to the next. ModalDimOverlay and ModalDimScope are gone; ModalLayerHold replaces the scope at the sites that still open a system message box. ModalDialog is what a dialog inherits in place of the window it lost: the panel background, the drawn header with its optional close button, and the dismissal gestures. isDismissible() governs Q and, next, the click outside -- one predicate because both gestures reach the same dialogs. Its default refuses, so a dialog opts in. requestDismiss() is what a gesture asks for, and ShipLayoutDialog overrides it with the one-step ladder its Q handler used to spell out, which is now reached by both. DialogDismiss.h folded into the base. The four dialogs the player meets keep their contents unchanged and lose their title bars: the recipe/schematic selection, blueprint selection, ship layout, and schematic choice dialogs. Placement moved with them -- placeOnSelectionPanel became getSelectionPanelAnchor, and the layer centers on that rectangle in window coordinates, with no global mapping and no frame height to guess at. Two things followed from dropping OS modality: the focus guard that hands focus back to the game world now also asks the layer whether a modal is open, and closeEvent refuses to close the window while one is, since its nested loop runs over widgets the window owns. The escape menu, the game-over and win screens, and the two name dialogs are still system dialogs; they are dimmed by a layer hold until they are converted next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
@@ -28,7 +28,6 @@
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "BlueprintLibrary.h"
|
||||
#include "DialogDismiss.h"
|
||||
#include "IconCaption.h"
|
||||
#include "ItemIconCache.h"
|
||||
|
||||
@@ -150,54 +149,28 @@ namespace
|
||||
BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library,
|
||||
ItemIconCache* itemIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
: ModalDialog(parent)
|
||||
, m_library(library)
|
||||
, m_itemIcons(itemIcons)
|
||||
{
|
||||
setWindowTitle(tr("Blueprints"));
|
||||
setModal(true);
|
||||
// Frameless: the dialog draws its own header row, so an OS title bar would only
|
||||
// repeat it (REQ-UI-BLUEPRINT-DIALOG). Square corners rather than rounded ones --
|
||||
// rounding a top-level window needs a translucent background, which is unreliable
|
||||
// on Windows. The border matches the build bar and the selection panel.
|
||||
setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
setStyleSheet(QStringLiteral(
|
||||
"BlueprintSelectionDialog { background-color: palette(window);"
|
||||
" border: 1px solid palette(mid); }"));
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx);
|
||||
mainLayout->setSpacing(kSpacingPx);
|
||||
|
||||
QHBoxLayout* headerLayout = new QHBoxLayout();
|
||||
headerLayout->setSpacing(kSpacingPx);
|
||||
|
||||
QLabel* titleLabel = new QLabel(tr("Blueprints"), this);
|
||||
QFont headerFont = titleLabel->font();
|
||||
headerFont.setBold(true);
|
||||
titleLabel->setFont(headerFont);
|
||||
headerLayout->addWidget(titleLabel);
|
||||
QHBoxLayout* headerLayout = addHeader(mainLayout, tr("Blueprints"), true);
|
||||
|
||||
// Dimmed and bold, the treatment the build button hotkey badges use, so the
|
||||
// shortcut reads as a reminder rather than a second title (REQ-UI-BUILD-COST).
|
||||
// Inserted right after the title, left of the stretch the header ends with.
|
||||
QLabel* hotkeyBadge = new QLabel(tr("Ctrl+V"), this);
|
||||
hotkeyBadge->setFont(headerFont);
|
||||
QFont badgeFont = hotkeyBadge->font();
|
||||
badgeFont.setBold(true);
|
||||
hotkeyBadge->setFont(badgeFont);
|
||||
QPalette badgePalette = hotkeyBadge->palette();
|
||||
badgePalette.setColor(hotkeyBadge->foregroundRole(),
|
||||
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||
hotkeyBadge->setPalette(badgePalette);
|
||||
headerLayout->addWidget(hotkeyBadge);
|
||||
|
||||
headerLayout->addStretch();
|
||||
|
||||
QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this);
|
||||
closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||
closeButton->setToolTip(tr("Close"));
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
headerLayout->addWidget(closeButton);
|
||||
|
||||
mainLayout->addLayout(headerLayout);
|
||||
headerLayout->insertWidget(1, hotkeyBadge);
|
||||
|
||||
QScrollArea* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
@@ -223,13 +196,11 @@ BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library,
|
||||
setFixedSize(gridWidth + style()->pixelMetric(QStyle::PM_ScrollBarExtent)
|
||||
+ 2 * kSpacingPx,
|
||||
gridHeight + kSmallButtonSizePx + 3 * kSpacingPx);
|
||||
|
||||
// A frameless dialog does not get Qt's automatic centering on its parent.
|
||||
if (parent)
|
||||
{
|
||||
const QRect parentRect = parent->window()->geometry();
|
||||
move(parentRect.center() - QPoint(width() / 2, height() / 2));
|
||||
}
|
||||
|
||||
bool BlueprintSelectionDialog::isDismissible() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int> BlueprintSelectionDialog::getChosenIndex() const
|
||||
@@ -237,19 +208,6 @@ std::optional<int> BlueprintSelectionDialog::getChosenIndex() const
|
||||
return m_chosenIndex;
|
||||
}
|
||||
|
||||
void BlueprintSelectionDialog::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
// Q closes with no card picked, exactly as the close button does. A build mode
|
||||
// running underneath is left alone: the dialog took the key, not the world
|
||||
// (REQ-UI-DIALOG-DISMISS, REQ-UI-BLUEPRINT-DIALOG).
|
||||
if (isDialogDismissKey(*event))
|
||||
{
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
|
||||
void BlueprintSelectionDialog::rebuildGrid()
|
||||
{
|
||||
// deleteLater, not delete: this runs from a delete button's own clicked signal, and
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QDialog>
|
||||
#include "ModalDialog.h"
|
||||
|
||||
class BlueprintLibrary;
|
||||
class ItemIconCache;
|
||||
class QGridLayout;
|
||||
class QWidget;
|
||||
|
||||
// The blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG): a frameless modal panel
|
||||
// showing every saved blueprint as a card in a scrolling two-column grid. The caller
|
||||
// pauses the game and raises the dim overlay while it is open.
|
||||
// The blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG): a modal panel showing every
|
||||
// saved blueprint as a card in a scrolling two-column grid. The caller pauses the game
|
||||
// while it is open.
|
||||
//
|
||||
// Clicking a card accepts the dialog and reports that blueprint's index; the caller
|
||||
// enters placement mode afterwards, so the dialog is already closed by then
|
||||
// (REQ-UI-BLUEPRINT-CARD). Deleting acts on the library immediately and leaves the
|
||||
// dialog open (REQ-UI-BLUEPRINT-DELETE). Escape, Q, and the close button dismiss it
|
||||
// with no other effect (REQ-UI-DIALOG-DISMISS).
|
||||
class BlueprintSelectionDialog : public QDialog
|
||||
// dialog open (REQ-UI-BLUEPRINT-DELETE). Escape, Q, the close button, and a click
|
||||
// outside dismiss it with no other effect (REQ-UI-DIALOG-DISMISS).
|
||||
class BlueprintSelectionDialog : public ModalDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -29,8 +29,7 @@ public:
|
||||
|
||||
std::optional<int> getChosenIndex() const;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
bool isDismissible() const override;
|
||||
|
||||
private:
|
||||
void rebuildGrid();
|
||||
|
||||
@@ -5,7 +5,8 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalDimOverlay.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalLayer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalPauseScope.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.h
|
||||
@@ -18,7 +19,6 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DialogDismiss.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||
@@ -44,7 +44,8 @@ SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalDimOverlay.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalLayer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.cpp
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <Qt>
|
||||
|
||||
// REQ-UI-DIALOG-DISMISS: Q dismisses an open dialog, beside the Escape that QDialog
|
||||
// already handles. The key is spelled here rather than in each dialog's key handler, so
|
||||
// the dialogs that take it cannot drift apart from one another.
|
||||
//
|
||||
// Ctrl must not be held, matching how the game world's table separates a chord from the
|
||||
// bare key (resolveKeyAction in lib/core/ControlAction.cpp). This deliberately stays out
|
||||
// of that table: the table answers what an input does in the player's current situation,
|
||||
// and a dialog has no situation -- it holds focus and takes the key whatever the world
|
||||
// beneath it is doing.
|
||||
inline bool isDialogDismissKey(const QKeyEvent& event)
|
||||
{
|
||||
return event.key() == Qt::Key_Q
|
||||
&& (event.modifiers() & Qt::ControlModifier) == 0;
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
#include "ShipLayoutDialog.h"
|
||||
#include "BuildingIconCache.h"
|
||||
#include "ItemIconCache.h"
|
||||
#include "ModalLayer.h"
|
||||
#include "ModalPauseScope.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
@@ -98,14 +99,18 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
// after the view for the same stacking reason as the panels above it.
|
||||
m_controlsPanel = new ControlsPanel(m_gameWorldView, this);
|
||||
|
||||
// Created last so it stacks above the other children; covers the whole window and
|
||||
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
||||
m_dimOverlay = new ModalDimOverlay(m_visuals.overlays.modalDim, this);
|
||||
// Created last so it stacks above the other children; covers the whole window,
|
||||
// dims the game behind every modal, and is what those modals are drawn on
|
||||
// (REQ-UI-MODAL-DIM, REQ-UI-MODAL-CHROME).
|
||||
m_modalLayer = new ModalLayer(m_visuals.overlays.modalDim, this);
|
||||
|
||||
m_gameWorldView->setFocus();
|
||||
|
||||
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
|
||||
if (newWidget && newWidget != m_gameWorldView && !QApplication::activeModalWidget())
|
||||
// A modal holds the focus while it is open, whether it is one of ours on the
|
||||
// layer or a system message box (REQ-UI-MODAL-CHROME).
|
||||
if (newWidget && newWidget != m_gameWorldView && !QApplication::activeModalWidget()
|
||||
&& !m_modalLayer->isActive())
|
||||
{
|
||||
m_gameWorldView->setFocus();
|
||||
}
|
||||
@@ -145,6 +150,15 @@ void MainWindow::resizeEvent(QResizeEvent* event)
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
// A modal on the layer runs a nested event loop over widgets this window owns, so
|
||||
// the window must outlive it. A modal window used to block the close outright; this
|
||||
// does the same for a modal that is only a widget (REQ-UI-MODAL-CHROME).
|
||||
if (m_modalLayer->isActive())
|
||||
{
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString path = QCoreApplication::applicationDirPath() + "/ship_layouts.toml";
|
||||
QFile file(path);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
@@ -173,7 +187,7 @@ void MainWindow::layoutPanels()
|
||||
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
||||
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
||||
m_gameWorldView->setGeometry(worldRect);
|
||||
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
||||
m_modalLayer->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
|
||||
@@ -223,10 +237,9 @@ void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEven
|
||||
{
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes,
|
||||
m_itemIcons.get(), m_buildingIcons.get(), this);
|
||||
dialog.exec();
|
||||
m_itemIcons.get(), m_buildingIcons.get(), m_modalLayer);
|
||||
m_modalLayer->execute(dialog);
|
||||
|
||||
// The command goes out unconditionally because the dialog cannot be dismissed: it
|
||||
// returns only once an option was clicked, so the index always names that option
|
||||
@@ -244,7 +257,7 @@ void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*e
|
||||
{
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Paused"));
|
||||
QPushButton* continueBtn = box.addButton(tr("Continue"), QMessageBox::AcceptRole);
|
||||
@@ -289,7 +302,7 @@ std::optional<GameConfig> MainWindow::reloadConfig()
|
||||
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
|
||||
m_modalLayer->setDimColor(m_visuals.overlays.modalDim);
|
||||
// The composed item squares carry the colors they were painted with, so they
|
||||
// are dropped for the new ones to take effect (REQ-UI-ITEM-ICON).
|
||||
m_itemIcons->clearPixmapCache();
|
||||
@@ -318,16 +331,15 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
|
||||
}
|
||||
}
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
ShipLayoutDialog dialog(&m_sim->getConfig(), schematicId, currentLayout,
|
||||
m_layoutBlueprints,
|
||||
std::move(unlockedModuleIds),
|
||||
m_gameWorldView->isDebugDrawEnabled(),
|
||||
m_itemIcons.get(), this);
|
||||
m_itemIcons.get(), m_modalLayer);
|
||||
// Opened from the panel's "Configure" button (REQ-MOD-UI-PREVIEW) or straight after
|
||||
// a schematic change (REQ-MOD-UI-AUTO-DIALOG), so it opens on the panel either way.
|
||||
placeOnSelectionPanel(dialog);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.getResult().has_value())
|
||||
if (m_modalLayer->execute(dialog, getSelectionPanelAnchor()) == QDialog::Accepted
|
||||
&& dialog.getResult().has_value())
|
||||
{
|
||||
std::shared_ptr<SetShipLayoutCommand> command =
|
||||
std::make_shared<SetShipLayoutCommand>();
|
||||
@@ -338,43 +350,18 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::placeOnSelectionPanel(QDialog& dialog) const
|
||||
QRect MainWindow::getSelectionPanelAnchor() const
|
||||
{
|
||||
// The panel is up whenever one of these modals opens -- they are opened from its own
|
||||
// controls, and it is shown whenever anything is selected (REQ-UI-EMPTY-SELECTION).
|
||||
// Were it not, there would be no rectangle to center on and Qt's own centering on
|
||||
// this window stands.
|
||||
if (!m_selectionPanel->isVisible()) { return; }
|
||||
|
||||
// The dialog has never been shown, so it is still at its default size until its
|
||||
// layout has run; centering it before that would use the wrong extent.
|
||||
dialog.adjustSize();
|
||||
const QSize dialogSize = dialog.size();
|
||||
// Were it not, there would be no rectangle to center on and the layer's own centering
|
||||
// stands (REQ-UI-PANEL-MODAL).
|
||||
if (!m_selectionPanel->isVisible()) { return QRect(); }
|
||||
|
||||
// The panel's live geometry, so a panel the player has dragged
|
||||
// (REQ-UI-SELECTION-PANEL-DRAG) carries the modal with it.
|
||||
const QRect panelRect(m_selectionPanel->mapToGlobal(QPoint(0, 0)),
|
||||
m_selectionPanel->size());
|
||||
const QRect windowRect(mapToGlobal(QPoint(0, 0)), size());
|
||||
|
||||
QPoint topLeft(panelRect.center().x() - dialogSize.width() / 2,
|
||||
panelRect.center().y() - dialogSize.height() / 2);
|
||||
|
||||
// Pushed back inside the window, never resized to fit (REQ-UI-PANEL-MODAL). The far
|
||||
// edge is clamped first and the near edge second, which is what aligns a dialog too
|
||||
// large for the window with the window's top-left corner rather than pushing it off
|
||||
// the opposite edge.
|
||||
topLeft.setX(qMax(windowRect.left(),
|
||||
qMin(topLeft.x(), windowRect.right() - dialogSize.width() + 1)));
|
||||
topLeft.setY(qMax(windowRect.top(),
|
||||
qMin(topLeft.y(), windowRect.bottom() - dialogSize.height() + 1)));
|
||||
|
||||
// Positions the dialog's frame, whose size is not known until it is first shown, so
|
||||
// the result sits low by the title bar height against a true center -- measuring it
|
||||
// would mean showing the dialog at the wrong place first. The move also marks the
|
||||
// dialog as positioned, which is what stops QDialog from centering it on this window
|
||||
// when it is shown.
|
||||
dialog.move(topLeft);
|
||||
// (REQ-UI-SELECTION-PANEL-DRAG) carries the modal with it. Panel and layer are both
|
||||
// children of this window, so the panel's geometry needs no mapping.
|
||||
return m_selectionPanel->geometry();
|
||||
}
|
||||
|
||||
void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
|
||||
@@ -427,7 +414,7 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
|
||||
// Held across both the selection dialog and any auto-opened layout dialog so the
|
||||
// dim stays continuously visible through that sequence (REQ-UI-MODAL-DIM).
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
|
||||
const BuildingType type = b ? b->type : s->type;
|
||||
// Captured as a copy: a queued command may drain during the modal dialog's
|
||||
@@ -443,9 +430,9 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
||||
bool autoOpenLayout = false;
|
||||
std::string chosenSchematic;
|
||||
RecipeSelectionDialog dialog(options, title, m_itemIcons.get(),
|
||||
m_buildingIcons.get(), this);
|
||||
placeOnSelectionPanel(dialog);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
||||
m_buildingIcons.get(), m_modalLayer);
|
||||
if (m_modalLayer->execute(dialog, getSelectionPanelAnchor()) == QDialog::Accepted
|
||||
&& dialog.getChosenId().has_value())
|
||||
{
|
||||
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
||||
command->id = event->buildingId;
|
||||
@@ -487,7 +474,7 @@ void MainWindow::handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent>
|
||||
// over to, so the dim never blinks off and the simulation is not resumed in between
|
||||
// (REQ-UI-MODAL-DIM).
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
@@ -503,14 +490,15 @@ void MainWindow::handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent>
|
||||
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> /*event*/)
|
||||
{
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
showBlueprintSelectionDialog();
|
||||
}
|
||||
|
||||
void MainWindow::showBlueprintSelectionDialog()
|
||||
{
|
||||
BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), this);
|
||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenIndex().has_value())
|
||||
BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(),
|
||||
m_modalLayer);
|
||||
if (m_modalLayer->execute(dialog) == QDialog::Accepted
|
||||
&& dialog.getChosenIndex().has_value())
|
||||
{
|
||||
// Entered after the dialog has closed, which is the order REQ-UI-BLUEPRINT-CARD
|
||||
// describes: clicking a card closes the dialog and enters placement mode.
|
||||
@@ -525,7 +513,7 @@ void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||
const int minutes = totalSeconds / 60;
|
||||
const int seconds = totalSeconds % 60;
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Game Over"));
|
||||
box.setText(tr("HQ destroyed!\nSurvival time: %1:%2")
|
||||
@@ -562,7 +550,7 @@ void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/)
|
||||
const int minutes = totalSeconds / 60;
|
||||
const int seconds = totalSeconds % 60;
|
||||
|
||||
ModalDimScope dim(*m_dimOverlay);
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Won!"));
|
||||
box.setText(tr("You collected all artifacts!\nSurvival time: %1:%2")
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "GameConfig.h"
|
||||
#include "GameOverEvent.h"
|
||||
#include "LayoutDialogRequestedEvent.h"
|
||||
#include "ModalDimOverlay.h"
|
||||
#include "ModalLayer.h"
|
||||
#include "WinEvent.h"
|
||||
#include "RecipeSelectionRequestedEvent.h"
|
||||
#include "SchematicChoicesAvailableEvent.h"
|
||||
@@ -36,7 +36,6 @@ class BlueprintLibrary;
|
||||
class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
class QCloseEvent;
|
||||
class QDialog;
|
||||
class QResizeEvent;
|
||||
|
||||
class MainWindow : public QWidget,
|
||||
@@ -84,15 +83,15 @@ private:
|
||||
const std::string& schematicId,
|
||||
const ShipLayoutConfig& currentLayout);
|
||||
|
||||
// Centers a modal opened from the selection panel on that panel, kept inside this
|
||||
// window (REQ-UI-PANEL-MODAL). Called on the constructed dialog before exec(), and
|
||||
// only for the two modals the panel opens.
|
||||
void placeOnSelectionPanel(QDialog& dialog) const;
|
||||
// The rectangle a modal opened from the selection panel is centered on, in this
|
||||
// window's coordinates, or a null rect when the panel is not up (REQ-UI-PANEL-MODAL).
|
||||
QRect getSelectionPanelAnchor() const;
|
||||
|
||||
// Runs the blueprint selection dialog and enters placement mode for whatever the
|
||||
// player picked (REQ-UI-BLUEPRINT-DIALOG). Holds no pause or dim scope of its own:
|
||||
// both callers already hold theirs, which is what keeps the dim continuous when a
|
||||
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
||||
// player picked (REQ-UI-BLUEPRINT-DIALOG). Holds no pause scope of its own: both
|
||||
// callers already hold theirs, and the save path also holds the layer, which is what
|
||||
// keeps the dim continuous when a confirmed save hands straight over to this dialog
|
||||
// (REQ-UI-MODAL-DIM).
|
||||
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.
|
||||
@@ -116,7 +115,7 @@ private:
|
||||
// The saved blueprints themselves; they have no widget of their own any more and
|
||||
// are reached through the two modal dialogs (REQ-UI-BLUEPRINT-DIALOG).
|
||||
std::unique_ptr<BlueprintLibrary> m_blueprintLibrary;
|
||||
ModalDimOverlay* m_dimOverlay = nullptr;
|
||||
ModalLayer* m_modalLayer = nullptr;
|
||||
|
||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
||||
|
||||
97
src/ui/ModalDialog.cpp
Normal file
97
src/ui/ModalDialog.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "ModalDialog.h"
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QChar>
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QString>
|
||||
|
||||
namespace
|
||||
{
|
||||
// The header's own metrics, matching the row the blueprint selection dialog drew
|
||||
// before this class existed (REQ-UI-BLUEPRINT-DIALOG).
|
||||
const int kSpacingPx = 8;
|
||||
const int kSmallButtonSizePx = 22;
|
||||
const QChar kCrossGlyph(0x00D7); // U+00D7 MULTIPLICATION SIGN
|
||||
|
||||
// Q dismisses an open dialog, beside the Escape that QDialog already handles
|
||||
// (REQ-UI-DIALOG-DISMISS). Ctrl must not be held, matching how the game world's
|
||||
// table separates a chord from the bare key (resolveKeyAction in
|
||||
// lib/core/ControlAction.cpp). This deliberately stays out of that table: the table
|
||||
// answers what an input does in the player's current situation, and a dialog has no
|
||||
// situation -- it holds focus and takes the key whatever the world beneath it is
|
||||
// doing.
|
||||
bool isDismissKey(const QKeyEvent& event)
|
||||
{
|
||||
return event.key() == Qt::Key_Q
|
||||
&& (event.modifiers() & Qt::ControlModifier) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
ModalDialog::ModalDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
// An ordinary child widget rather than a window: the layer places it and the dim
|
||||
// behind it is the same widget's paint (REQ-UI-MODAL-CHROME). Square corners rather
|
||||
// than rounded ones -- rounding needs a translucent background, which is unreliable
|
||||
// on Windows. The border matches the build bar and the selection panel.
|
||||
setWindowFlags(Qt::Widget);
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
// A type selector, so it reaches every subclass but none of the child widgets
|
||||
// inside them, which draw themselves.
|
||||
setStyleSheet(QStringLiteral(
|
||||
"ModalDialog { background-color: palette(window);"
|
||||
" border: 1px solid palette(mid); }"));
|
||||
}
|
||||
|
||||
bool ModalDialog::isDismissible() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void ModalDialog::requestDismiss()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
|
||||
QHBoxLayout* ModalDialog::addHeader(QBoxLayout* mainLayout, const QString& title,
|
||||
bool withCloseButton)
|
||||
{
|
||||
QHBoxLayout* headerLayout = new QHBoxLayout();
|
||||
headerLayout->setSpacing(kSpacingPx);
|
||||
|
||||
QLabel* titleLabel = new QLabel(title, this);
|
||||
QFont headerFont = titleLabel->font();
|
||||
headerFont.setBold(true);
|
||||
titleLabel->setFont(headerFont);
|
||||
headerLayout->addWidget(titleLabel);
|
||||
|
||||
// The stretch is added before the close button so a subclass inserting after the
|
||||
// title lands left of it, where the blueprint dialog's hotkey badge belongs.
|
||||
headerLayout->addStretch();
|
||||
|
||||
if (withCloseButton)
|
||||
{
|
||||
QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this);
|
||||
closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx);
|
||||
closeButton->setToolTip(tr("Close"));
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
headerLayout->addWidget(closeButton);
|
||||
}
|
||||
|
||||
mainLayout->insertLayout(0, headerLayout);
|
||||
return headerLayout;
|
||||
}
|
||||
|
||||
void ModalDialog::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (isDismissible() && isDismissKey(*event))
|
||||
{
|
||||
requestDismiss();
|
||||
return;
|
||||
}
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
48
src/ui/ModalDialog.h
Normal file
48
src/ui/ModalDialog.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QBoxLayout;
|
||||
class QHBoxLayout;
|
||||
class QKeyEvent;
|
||||
class QString;
|
||||
|
||||
// Base of every modal the player meets while playing (REQ-UI-MODAL-CHROME). A modal is
|
||||
// not an operating system window here: it is an ordinary child widget, hosted and placed
|
||||
// by the ModalLayer that also paints the dim it sits on, so it has no title bar, no
|
||||
// window border, and no window-manager close. What a window used to supply, this class
|
||||
// supplies instead -- the panel background, the drawn header, and the dismissal gestures.
|
||||
//
|
||||
// It stays a QDialog for accept()/reject()/result() and the Escape handling built into
|
||||
// them; only the window-ness is dropped (Qt::Widget flags). Nothing calls exec() on it:
|
||||
// ModalLayer::execute() runs the modal loop, so that the layer knows what is open.
|
||||
class ModalDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ModalDialog(QWidget* parent = nullptr);
|
||||
|
||||
// Whether Q and a click outside this dialog dismiss it (REQ-UI-DIALOG-DISMISS). One
|
||||
// predicate for both gestures because they reach exactly the same dialogs. The
|
||||
// default refuses both, so a dialog takes the gestures only by saying so: the two
|
||||
// name dialogs must let Q through as a character, and the schematic choice dialog
|
||||
// has no way out but choosing (REQ-DEF-SCHEMATIC-DROP).
|
||||
virtual bool isDismissible() const;
|
||||
|
||||
public slots:
|
||||
// What a dismissal does, whichever gesture asked for it. Cancelling is the default;
|
||||
// a dialog with modes of its own overrides this to back out of them one at a time
|
||||
// (ShipLayoutDialog, REQ-UI-DIALOG-DISMISS).
|
||||
virtual void requestDismiss();
|
||||
|
||||
protected:
|
||||
// Adds the drawn header row -- the title, and a close button at the far right when
|
||||
// asked for -- as the first row of mainLayout, and returns it so a subclass can
|
||||
// insert its own widgets after the title (REQ-UI-MODAL-CHROME). The close button
|
||||
// rejects the dialog, which is what the window-manager close used to do.
|
||||
QHBoxLayout* addHeader(QBoxLayout* mainLayout, const QString& title,
|
||||
bool withCloseButton);
|
||||
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
#include "ModalDimOverlay.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
ModalDimOverlay::ModalDimOverlay(const QColor& dimColor, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_dimColor(dimColor)
|
||||
{
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
hide();
|
||||
}
|
||||
|
||||
void ModalDimOverlay::pushModal()
|
||||
{
|
||||
if (m_modalDepth++ == 0)
|
||||
{
|
||||
raise();
|
||||
show();
|
||||
// Force an immediate synchronous paint so the scrim is visible before the
|
||||
// caller enters a blocking dialog exec() (no undimmed frame flashes through).
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void ModalDimOverlay::popModal()
|
||||
{
|
||||
if (m_modalDepth > 0 && --m_modalDepth == 0)
|
||||
{
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
void ModalDimOverlay::setDimColor(const QColor& dimColor)
|
||||
{
|
||||
m_dimColor = dimColor;
|
||||
if (isVisible())
|
||||
{
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ModalDimOverlay::paintEvent(QPaintEvent* /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), m_dimColor);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QWidget>
|
||||
|
||||
class QPaintEvent;
|
||||
|
||||
// A window-wide, semi-transparent scrim drawn over the entire game window while a
|
||||
// modal dialog or menu is open, behind that modal (REQ-UI-MODAL-DIM). It is a child
|
||||
// of the main window covering its full rect and is transparent to mouse events, so it
|
||||
// only dims the game presentation and never intercepts input.
|
||||
//
|
||||
// Visibility is reference-counted via pushModal()/popModal() so that a single dim is
|
||||
// shown across nested or back-to-back modals (e.g. the recipe selection dialog that
|
||||
// immediately opens the layout dialog) rather than flickering or stacking overlays.
|
||||
class ModalDimOverlay : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ModalDimOverlay(const QColor& dimColor, QWidget* parent);
|
||||
|
||||
// Raise + show on the first active modal; hide when the last one closes.
|
||||
void pushModal();
|
||||
void popModal();
|
||||
|
||||
// Update the dim color (e.g. after a config reload on Restart, REQ-CFG-RELOAD).
|
||||
void setDimColor(const QColor& dimColor);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
QColor m_dimColor;
|
||||
int m_modalDepth = 0;
|
||||
};
|
||||
|
||||
// RAII guard: shows the dim overlay for the duration of a scope (typically around a
|
||||
// blocking dialog exec()) and hides it (via reference count) on scope exit.
|
||||
class ModalDimScope
|
||||
{
|
||||
public:
|
||||
explicit ModalDimScope(ModalDimOverlay& overlay)
|
||||
: m_overlay(overlay)
|
||||
{
|
||||
m_overlay.pushModal();
|
||||
}
|
||||
|
||||
~ModalDimScope()
|
||||
{
|
||||
m_overlay.popModal();
|
||||
}
|
||||
|
||||
ModalDimScope(const ModalDimScope&) = delete;
|
||||
ModalDimScope& operator=(const ModalDimScope&) = delete;
|
||||
|
||||
private:
|
||||
ModalDimOverlay& m_overlay;
|
||||
};
|
||||
140
src/ui/ModalLayer.cpp
Normal file
140
src/ui/ModalLayer.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "ModalLayer.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QFrame>
|
||||
#include <QMetaObject>
|
||||
#include <QPainter>
|
||||
#include <QPoint>
|
||||
#include <QScrollArea>
|
||||
#include <QSize>
|
||||
|
||||
#include "ModalDialog.h"
|
||||
|
||||
ModalLayer::ModalLayer(const QColor& dimColor, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_dimColor(dimColor)
|
||||
{
|
||||
hide();
|
||||
}
|
||||
|
||||
int ModalLayer::execute(ModalDialog& content, const QRect& anchorRect)
|
||||
{
|
||||
// Hosted in a scroll area so a modal larger than the window is reached by scrolling
|
||||
// instead of being cut off by the window edge (REQ-UI-MODAL-CHROME). When the
|
||||
// content fits -- which is the normal case -- the host is exactly its size and no
|
||||
// scroll bar appears, so the player sees the modal alone.
|
||||
QScrollArea* host = new QScrollArea(this);
|
||||
host->setFrameShape(QFrame::NoFrame);
|
||||
host->setWidgetResizable(false);
|
||||
host->setWidget(&content);
|
||||
|
||||
m_stack.push_back(&content);
|
||||
updateVisibility();
|
||||
raise();
|
||||
place(*host, content, anchorRect);
|
||||
host->show();
|
||||
content.show();
|
||||
content.setFocus();
|
||||
|
||||
// The dialog's own loop, run here rather than by QDialog::exec(), so the layer knows
|
||||
// what is open and can place it, dim behind it, and take the clicks beside it.
|
||||
QEventLoop loop;
|
||||
const QMetaObject::Connection connection =
|
||||
connect(&content, &QDialog::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
disconnect(connection);
|
||||
|
||||
// takeWidget() before the host goes: the scroll area owns what it is given, and
|
||||
// every modal here is a local of its caller. It hands the content back parentless.
|
||||
host->takeWidget();
|
||||
content.hide();
|
||||
delete host;
|
||||
|
||||
m_stack.erase(std::remove(m_stack.begin(), m_stack.end(), &content), m_stack.end());
|
||||
updateVisibility();
|
||||
return content.result();
|
||||
}
|
||||
|
||||
bool ModalLayer::isActive() const
|
||||
{
|
||||
return !m_stack.empty();
|
||||
}
|
||||
|
||||
void ModalLayer::addHold()
|
||||
{
|
||||
++m_holdCount;
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
void ModalLayer::removeHold()
|
||||
{
|
||||
if (m_holdCount > 0)
|
||||
{
|
||||
--m_holdCount;
|
||||
updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
void ModalLayer::setDimColor(const QColor& dimColor)
|
||||
{
|
||||
m_dimColor = dimColor;
|
||||
if (isVisible())
|
||||
{
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ModalLayer::paintEvent(QPaintEvent* /*event*/)
|
||||
{
|
||||
// One dim however many modals are stacked (REQ-UI-MODAL-DIM): the modals above it
|
||||
// draw their own opaque background, so nesting darkens nothing twice.
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), m_dimColor);
|
||||
}
|
||||
|
||||
void ModalLayer::place(QScrollArea& host, ModalDialog& content,
|
||||
const QRect& anchorRect) const
|
||||
{
|
||||
// The content has never been shown, so it is still at its default size until its
|
||||
// layout has run; sizing it before that would use the wrong extent.
|
||||
content.adjustSize();
|
||||
|
||||
const QSize hostSize = content.size().boundedTo(size());
|
||||
host.resize(hostSize);
|
||||
|
||||
const QRect anchor = anchorRect.isNull() ? rect() : anchorRect;
|
||||
QPoint topLeft(anchor.center().x() - hostSize.width() / 2,
|
||||
anchor.center().y() - hostSize.height() / 2);
|
||||
|
||||
// Pushed back inside the layer, which is the game window (REQ-UI-PANEL-MODAL). The
|
||||
// far edge is clamped first and the near edge second, which is what aligns a modal
|
||||
// as large as the window with the window's top-left corner rather than pushing it
|
||||
// off the opposite edge.
|
||||
topLeft.setX(qMax(0, qMin(topLeft.x(), width() - hostSize.width())));
|
||||
topLeft.setY(qMax(0, qMin(topLeft.y(), height() - hostSize.height())));
|
||||
host.move(topLeft);
|
||||
}
|
||||
|
||||
void ModalLayer::updateVisibility()
|
||||
{
|
||||
const bool shouldShow = !m_stack.empty() || m_holdCount > 0;
|
||||
if (shouldShow == isVisible())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldShow)
|
||||
{
|
||||
raise();
|
||||
show();
|
||||
// Force an immediate synchronous paint so the dim is up before whatever the
|
||||
// caller does next; no undimmed frame flashes through.
|
||||
repaint();
|
||||
}
|
||||
else
|
||||
{
|
||||
hide();
|
||||
}
|
||||
}
|
||||
82
src/ui/ModalLayer.h
Normal file
82
src/ui/ModalLayer.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QColor>
|
||||
#include <QRect>
|
||||
#include <QWidget>
|
||||
|
||||
class ModalDialog;
|
||||
class QPaintEvent;
|
||||
class QScrollArea;
|
||||
|
||||
// The one surface every modal is shown on (REQ-UI-MODAL-CHROME, REQ-UI-MODAL-DIM): a
|
||||
// child of the main window covering its whole rect, which paints the dim, hosts the
|
||||
// open modal, and runs its modal loop. Because it is a widget of the window rather than
|
||||
// a window of its own, it receives the clicks that land beside the modal -- which a
|
||||
// blocked window would never see -- and that is what makes the click dismissal possible
|
||||
// (REQ-UI-DIALOG-DISMISS).
|
||||
//
|
||||
// Modals nest: the layer keeps a stack, shows one dim for all of them, and hides itself
|
||||
// when the last one closes. It is shown while the stack is non-empty or a hold is taken.
|
||||
class ModalLayer : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ModalLayer(const QColor& dimColor, QWidget* parent);
|
||||
|
||||
// Shows content on this layer and runs it until it accepts or rejects, returning its
|
||||
// QDialog result code. anchorRect (in this layer's coordinates, which are the main
|
||||
// window's) is what the content is centered on; a null rect centers it on the layer
|
||||
// itself (REQ-UI-PANEL-MODAL).
|
||||
int execute(ModalDialog& content, const QRect& anchorRect = QRect());
|
||||
|
||||
// Whether a modal is open. The main window asks before handing focus back to the
|
||||
// game world, which it must not do while a modal holds it.
|
||||
bool isActive() const;
|
||||
|
||||
// Keeps the layer shown with nothing on it, so that one modal handing straight over
|
||||
// to another does not blink the dim off in between (REQ-UI-MODAL-DIM).
|
||||
void addHold();
|
||||
void removeHold();
|
||||
|
||||
// Update the dim color (e.g. after a config reload on Restart, REQ-CFG-RELOAD).
|
||||
void setDimColor(const QColor& dimColor);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
// Sizes content to what it asks for, capped at the layer, and centers it on
|
||||
// anchorRect (REQ-UI-PANEL-MODAL).
|
||||
void place(QScrollArea& host, ModalDialog& content, const QRect& anchorRect) const;
|
||||
|
||||
void updateVisibility();
|
||||
|
||||
QColor m_dimColor;
|
||||
std::vector<ModalDialog*> m_stack; // bottom-most first; back() is the open one
|
||||
int m_holdCount = 0;
|
||||
};
|
||||
|
||||
// RAII guard for addHold()/removeHold().
|
||||
class ModalLayerHold
|
||||
{
|
||||
public:
|
||||
explicit ModalLayerHold(ModalLayer& layer)
|
||||
: m_layer(layer)
|
||||
{
|
||||
m_layer.addHold();
|
||||
}
|
||||
|
||||
~ModalLayerHold()
|
||||
{
|
||||
m_layer.removeHold();
|
||||
}
|
||||
|
||||
ModalLayerHold(const ModalLayerHold&) = delete;
|
||||
ModalLayerHold& operator=(const ModalLayerHold&) = delete;
|
||||
|
||||
private:
|
||||
ModalLayer& m_layer;
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
// exit it restores the snapshotted speed and rebases the render frame timer, so the
|
||||
// wall time the player spent in the dialog is not converted into simulation ticks.
|
||||
//
|
||||
// Pairs with ModalDimScope, which the same call sites use for the dim overlay.
|
||||
// Pairs with ModalLayer, which the same call sites use to show the modal itself.
|
||||
//
|
||||
// Two escape hatches for the paths that must not simply restore at scope exit:
|
||||
// restore() — restore now instead of at scope exit, for when more work has to run
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingType.h"
|
||||
#include "DialogDismiss.h"
|
||||
#include "DisplayName.h"
|
||||
#include "GameConfig.h"
|
||||
#include "OptionButton.h"
|
||||
@@ -109,11 +108,8 @@ RecipeSelectionDialog::RecipeSelectionDialog(
|
||||
const std::vector<RecipeSelectionOption>& options,
|
||||
const QString& title, ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
: ModalDialog(parent)
|
||||
{
|
||||
setWindowTitle(title);
|
||||
setModal(true);
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// One vertical column of buttons, each stating what it makes (REQ-UI-SELECT-OPTIONS).
|
||||
@@ -177,6 +173,16 @@ RecipeSelectionDialog::RecipeSelectionDialog(
|
||||
listLayout->addStretch(1);
|
||||
|
||||
scrollArea->setWidget(list);
|
||||
|
||||
// Added last so the header sits above a list whose height is already settled; the
|
||||
// modal draws its own title and close button, having no window frame to carry them
|
||||
// (REQ-UI-MODAL-CHROME).
|
||||
addHeader(mainLayout, title, true);
|
||||
}
|
||||
|
||||
bool RecipeSelectionDialog::isDismissible() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::string> RecipeSelectionDialog::getChosenId() const
|
||||
@@ -184,18 +190,6 @@ std::optional<std::string> RecipeSelectionDialog::getChosenId() const
|
||||
return m_chosenId;
|
||||
}
|
||||
|
||||
void RecipeSelectionDialog::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
// Q dismisses, leaving the recipe as it was -- the same nothing that Escape and the
|
||||
// close button do, since no option was clicked (REQ-UI-DIALOG-DISMISS).
|
||||
if (isDialogDismissKey(*event))
|
||||
{
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
|
||||
void RecipeSelectionDialog::onOptionClicked(int index)
|
||||
{
|
||||
if (index >= 0 && index < static_cast<int>(m_optionIds.size()))
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
#include "BuildingType.h"
|
||||
#include "ModalDialog.h"
|
||||
#include "RecipeLineRow.h"
|
||||
|
||||
struct GameConfig;
|
||||
@@ -39,9 +39,10 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
|
||||
BuildingType type, Simulation& sim, const GameConfig& config);
|
||||
|
||||
// Modal dialog listing the options in one vertical column (REQ-UI-SELECT-OPTIONS). The
|
||||
// game is paused by the caller while it is open. Clicking an option selects it
|
||||
// and closes the dialog; dismissing it (close/Esc/Q) leaves no choice.
|
||||
class RecipeSelectionDialog : public QDialog
|
||||
// game is paused by the caller while it is open. Clicking an option selects it and closes
|
||||
// the dialog; dismissing it (close button, Escape, Q, or a click outside) leaves no
|
||||
// choice (REQ-UI-DIALOG-DISMISS).
|
||||
class RecipeSelectionDialog : public ModalDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -53,8 +54,7 @@ public:
|
||||
|
||||
std::optional<std::string> getChosenId() const;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
bool isDismissible() const override;
|
||||
|
||||
private:
|
||||
void onOptionClicked(int index);
|
||||
|
||||
@@ -44,15 +44,14 @@ SchematicChoiceDialog::SchematicChoiceDialog(
|
||||
const RecipesConfig& recipes, ItemIconCache* itemIcons,
|
||||
BuildingIconCache* buildingIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
: ModalDialog(parent)
|
||||
, m_chosenIndex(0)
|
||||
{
|
||||
setWindowTitle(tr("Schematic Drop"));
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowCloseButtonHint);
|
||||
setModal(true);
|
||||
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
|
||||
// Its own title line, and the only one: a modal draws its title rather than wearing
|
||||
// one (REQ-UI-MODAL-CHROME), and this dialog carries no close button because there
|
||||
// is no way out but choosing (REQ-DEF-SCHEMATIC-DROP).
|
||||
QLabel* titleLabel = new QLabel(tr("Choose a schematic to unlock:"), this);
|
||||
QFont titleFont = titleLabel->font();
|
||||
titleFont.setPointSize(titleFont.pointSize() + 2);
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "ModalDialog.h"
|
||||
#include "SchematicChoiceOption.h"
|
||||
|
||||
struct RecipesConfig;
|
||||
class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
|
||||
// The drop's choice dialog (REQ-DEF-SCHEMATIC-DROP). Unlike every other dialog it
|
||||
// cannot be dismissed: clicking an option is the only way out, so getChosenIndex()
|
||||
// always names an option the player picked, and exec() only ever returns Accepted.
|
||||
class SchematicChoiceDialog : public QDialog
|
||||
// The drop's choice dialog (REQ-DEF-SCHEMATIC-DROP). Unlike every other dialog it cannot
|
||||
// be dismissed: clicking an option is the only way out, so getChosenIndex() always names
|
||||
// an option the player picked, and it only ever finishes Accepted. It inherits the base's
|
||||
// refusal of Q and of a click outside (ModalDialog::isDismissible) and adds the refusal
|
||||
// of Escape below.
|
||||
class SchematicChoiceDialog : public ModalDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -30,9 +31,8 @@ public:
|
||||
public slots:
|
||||
// Refuses the dismissal (REQ-DEF-SCHEMATIC-DROP): the drop is a reward the player
|
||||
// has earned, and leaving without choosing would either forfeit it or award the
|
||||
// option that happens to be first. Escape, Alt+F4, and the window manager's close
|
||||
// all funnel through QDialog::reject(), so declining it here turns away every one of
|
||||
// them at once rather than swallowing keys one at a time.
|
||||
// option that happens to be first. Escape funnels through QDialog::reject(), so
|
||||
// declining it here turns it away without swallowing keys one at a time.
|
||||
void reject() override;
|
||||
|
||||
private:
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
|
||||
#include "DialogDismiss.h"
|
||||
#include "DisplayName.h"
|
||||
#include "OptionButton.h"
|
||||
#include "ProductionRules.h"
|
||||
@@ -421,7 +420,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
bool debugDraw,
|
||||
ItemIconCache* itemIcons,
|
||||
QWidget* parent)
|
||||
: QDialog(parent)
|
||||
: ModalDialog(parent)
|
||||
, m_config(config)
|
||||
, m_itemIcons(itemIcons)
|
||||
, m_shipId(shipId)
|
||||
@@ -435,9 +434,6 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
, m_statsPanel(nullptr)
|
||||
, m_debugDraw(debugDraw)
|
||||
{
|
||||
setWindowTitle(tr("Configure Ship Layout"));
|
||||
setModal(true);
|
||||
|
||||
// Find the ship's layout grid.
|
||||
const ShipDef* shipDef = config->ships.findShipDef(shipId);
|
||||
if (shipDef)
|
||||
@@ -473,6 +469,10 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
||||
// --- UI layout ---
|
||||
QVBoxLayout* outerLayout = new QVBoxLayout(this);
|
||||
|
||||
// The dialog's own title line; it carries no close button, having Confirm and Cancel
|
||||
// of its own at the bottom (REQ-UI-MODAL-CHROME, REQ-MOD-UI-DIALOG).
|
||||
addHeader(outerLayout, tr("Configure Ship Layout"), false);
|
||||
|
||||
// Top: grid widget.
|
||||
LayoutGridWidget* gridW = new LayoutGridWidget(this, this);
|
||||
gridW->setGridData(&m_grid, m_rows, m_cols, &m_placedModules, m_config);
|
||||
@@ -682,21 +682,26 @@ void ShipLayoutDialog::keyPressEvent(QKeyEvent* event)
|
||||
}
|
||||
updateGridWidget();
|
||||
}
|
||||
else if (isDialogDismissKey(*event))
|
||||
else
|
||||
{
|
||||
// Q backs out one level per press, as it does in the game world: the module
|
||||
// being placed, then remove mode, then the dialog itself, which discards the
|
||||
// session (REQ-UI-DIALOG-DISMISS). Both mode exits go through the handler the
|
||||
// button uses, so the key and the button can never leave different state behind.
|
||||
// Q among them, which the base turns into requestDismiss().
|
||||
ModalDialog::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
bool ShipLayoutDialog::isDismissible() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::requestDismiss()
|
||||
{
|
||||
// Both mode exits go through the handler the buttons use, so a dismissal and a
|
||||
// button can never leave different state behind (REQ-MOD-PLACEMENT, REQ-MOD-REMOVE).
|
||||
if (m_activeModuleIndex.has_value()) { onModuleButtonClicked(*m_activeModuleIndex); }
|
||||
else if (m_removeMode) { onRemoveButtonClicked(); }
|
||||
else { onCancel(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ShipLayoutDialog::onModuleButtonClicked(int index)
|
||||
{
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPoint>
|
||||
|
||||
#include "GameConfig.h"
|
||||
#include "ModalDialog.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipLayoutBlueprint.h"
|
||||
@@ -20,7 +20,7 @@ class RecipeLineRow;
|
||||
class SectionBox;
|
||||
class ShipStatsPanel;
|
||||
|
||||
class ShipLayoutDialog : public QDialog
|
||||
class ShipLayoutDialog : public ModalDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -38,6 +38,15 @@ public:
|
||||
|
||||
std::optional<ShipLayoutConfig> getResult() const;
|
||||
|
||||
bool isDismissible() const override;
|
||||
|
||||
public slots:
|
||||
// Backs out one level per dismissal, as Q does in the game world: the module being
|
||||
// placed, then remove mode, then the dialog itself, which discards the session
|
||||
// (REQ-UI-DIALOG-DISMISS). So a single press or click never both leaves a mode and
|
||||
// throws the session away.
|
||||
void requestDismiss() override;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
|
||||
@@ -47,7 +56,7 @@ signals:
|
||||
private slots:
|
||||
void onModuleButtonClicked(int index);
|
||||
// Enters remove mode, or leaves it when it is already active (REQ-MOD-REMOVE).
|
||||
// Reached by the Remove button and by the Q key, which leaves the mode on its way
|
||||
// Reached by the Remove button and by a dismissal, which leaves the mode on its way
|
||||
// out of the dialog (REQ-UI-DIALOG-DISMISS).
|
||||
void onRemoveButtonClicked();
|
||||
void onConfirm();
|
||||
|
||||
Reference in New Issue
Block a user