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:
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user