Files
dota_factory/src/ui/ModalLayer.h
Malte Langkabel b51a9c5c77 give the keyboard back when a modal closes
Hiding the layer takes the focus off whatever stood on it and leaves the
window with no focus widget at all, so the world view answered no keys
afterwards: Escape opened the menu once, closed it, and then did nothing.
Each modal now notes what held the keyboard when it opened and gives it
back on the way out -- the world view for the first of a stack, the modal
beneath for one above it, which was dead the same way.

The window's own focus guard now also catches the focus going nowhere,
rather than only landing on the wrong widget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-18 22:11:04 +02:00

118 lines
4.5 KiB
C++

#pragma once
#include <vector>
#include <QColor>
#include <QPoint>
#include <QPointer>
#include <QRect>
#include <QWidget>
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<QWidget> 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<HostedModal> 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;
};