#pragma once #include #include #include #include #include #include class ModalDialog; class QMouseEvent; 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: // 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 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 // 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; void mousePressEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* 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; // What held the keyboard when this modal opened, given back when it closes. // Hiding the layer, or closing a modal a second one was opened from, leaves the // window with no focus widget at all, and a window with none answers no keys -- // Escape included, which is what opens the menu (REQ-UI-GAME-MENU). A QPointer // because a modal may outlive what it took the focus from. QPointer focusBefore; }; // 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(); // Whether a point in this layer's coordinates lies beyond the open modal. A click on // an inert part of the modal -- a label, the space between two controls -- arrives // here too, propagated by the widget that ignored it, so where the click landed is // what decides, never that the layer received it. bool isOutsideOpenModal(const QPoint& position) const; QColor m_dimColor; // Set by a left press that landed outside the open modal, so that only a press and a // release both outside dismiss it (REQ-UI-DIALOG-DISMISS). bool m_pressedOutside = false; std::vector 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; };