give the escape menu and the name prompts the game's own frame

The last dialogs wearing system chrome were the three QMessageBoxes -- the
escape menu, the game-over and win screens -- and the two QInputDialogs that
name a blueprint. They are the ones the player meets at the sharpest moments
of a run, and they looked like alerts from the operating system.

MessageDialog and NameInputDialog replace them on the layer. The message
dialog names its buttons by the index addButton hands back and reports which
was clicked, with Escape standing for a button the caller nominates rather
than for a dismissal of its own -- Continue in the escape menu, as it was
explicitly set to before, and Quit on the two state screens, which is where
the reject role sent it. Neither dialog takes Q or a click outside: every
button is a decision, and a half-typed name is work in progress
(REQ-UI-DIALOG-DISMISS).

Two things the layer needed for the nested case: it now tracks the scroll
area each modal is shown in, so a modal opened with no anchor centers on the
one it was opened from rather than on the window -- which is where the
Create Blueprint prompt belongs (REQ-UI-PANEL-MODAL) -- and findFor() walks
a widget's parents to the layer, so the blueprint panel buried in the layout
dialog can open a modal without every widget in between carrying a pointer.

The three error boxes stay system dialogs on purpose: config load, config
reload, and blueprint file load all report a failure that may leave nothing
to draw on (REQ-UI-MODAL-CHROME).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-18 12:14:31 +02:00
parent f03003194f
commit 9b88deff69
9 changed files with 333 additions and 60 deletions

View File

@@ -24,12 +24,18 @@ class ModalLayer : public QWidget
Q_OBJECT
public:
// The layer a widget is shown on, found by walking up its parents, or nullptr when
// it is not on one. How a widget deep inside a modal reaches the layer to open a
// second modal on it, without every widget between them having to carry a pointer.
static ModalLayer* findFor(const QWidget& widget);
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).
// window's) is what the content is centered on. A null rect centers it on the modal
// it was opened from, and on the layer itself when it is the first one open
// (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
@@ -48,15 +54,24 @@ protected:
void paintEvent(QPaintEvent* event) override;
private:
// A modal and the scroll area it is shown in. The host is what the layer places and
// what the player sees the edges of, so it is also the rectangle "outside the modal"
// is measured against.
struct HostedModal
{
ModalDialog* content;
QScrollArea* host;
};
// 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;
QColor m_dimColor;
std::vector<HostedModal> m_stack; // bottom-most first; back() is the open one
int m_holdCount = 0;
};
// RAII guard for addHold()/removeHold().