#include "ModalLayer.h" #include #include #include #include #include #include #include #include #include #include #include "ModalDialog.h" ModalLayer* ModalLayer::findFor(const QWidget& widget) { for (QWidget* candidate = widget.parentWidget(); candidate != nullptr; candidate = candidate->parentWidget()) { ModalLayer* layer = qobject_cast(candidate); if (layer != nullptr) { return layer; } } return nullptr; } 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. // Noted before anything here touches the focus, and given back when this modal // closes: the game world beneath for the first modal of a stack, and the modal it was // opened from for one above that. QWidget* const focusBefore = QApplication::focusWidget(); QScrollArea* host = new QScrollArea(this); host->setFrameShape(QFrame::NoFrame); host->setWidgetResizable(false); host->setWidget(&content); // Resolved before the new modal joins the stack, so a null rect means the modal it // was opened from -- the Create Blueprint dialog opens on the layout dialog beneath // it (REQ-UI-PANEL-MODAL). const QRect anchor = !anchorRect.isNull() ? anchorRect : (m_stack.empty() ? rect() : m_stack.back().host->geometry()); m_stack.push_back(HostedModal{ &content, host, focusBefore }); // Placed and shown before the layer itself is, so that the frame the layer first // paints already carries the modal. Showing the layer first put the dim on screen a // frame before anything stood on it, which read as a black flash // (REQ-UI-MODAL-DIM). Placing works while the layer is hidden: its geometry is the // window's whether or not it is shown, and a child shown under a hidden parent // appears with it. raise(); place(*host, content, anchor); host->show(); content.show(); updateVisibility(); // The widget the modal names where it names one -- a name dialog puts the caret in // its line edit -- and the modal itself otherwise, so keys reach it and not the game // world behind (REQ-UI-MODAL-CHROME). QWidget* focusTarget = content.getInitialFocusWidget(); if (focusTarget == nullptr) { focusTarget = &content; } focusTarget->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; QPointer focusAfter; for (std::size_t i = m_stack.size(); i > 0; --i) { if (m_stack[i - 1].content == &content) { focusAfter = m_stack[i - 1].focusBefore; m_stack.erase(m_stack.begin() + static_cast(i - 1)); break; } } updateVisibility(); // After the layer is hidden, not before: hiding a widget takes the focus off // everything on it, which would undo this again. if (!focusAfter.isNull()) { focusAfter->setFocus(); } 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::mousePressEvent(QMouseEvent* event) { m_pressedOutside = event->button() == Qt::LeftButton && isOutsideOpenModal(event->pos()); // Taken whether or not it dismisses anything: the window behind a modal receives no // input, and closing the modal does not turn this click into one for what lies under // it (REQ-UI-DIALOG-DISMISS). event->accept(); } void ModalLayer::mouseReleaseEvent(QMouseEvent* event) { const bool pressedOutside = m_pressedOutside; m_pressedOutside = false; event->accept(); if (event->button() != Qt::LeftButton || !pressedOutside || !isOutsideOpenModal(event->pos()) || m_stack.empty()) { return; } // A drag that began inside the modal never gets here -- the widget it began on keeps // the release -- so no gesture ends by discarding the modal it was made in. ModalDialog* openModal = m_stack.back().content; if (openModal->isDismissible()) { openModal->requestDismiss(); } } bool ModalLayer::isOutsideOpenModal(const QPoint& position) const { if (m_stack.empty()) { return true; } return !m_stack.back().host->geometry().contains(position); } 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); QPoint topLeft(anchorRect.center().x() - hostSize.width() / 2, anchorRect.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(); // Painted at once rather than at the next paint event, so no undimmed frame of // the world flashes through in between. The modal standing on the layer is // already placed and shown by now, so this one paint puts up both (execute()). repaint(); } else { hide(); } }