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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user