diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index ace1de1..db1ddf5 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -107,8 +107,11 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) { // A modal holds the focus while it is open, whether it is one of ours on the - // layer or a system message box (REQ-UI-MODAL-CHROME). - if (newWidget && newWidget != m_gameWorldView && !QApplication::activeModalWidget() + // layer or a system message box (REQ-UI-MODAL-CHROME). A null widget -- the focus + // going nowhere at all -- is caught too: the world view is what answers keys when + // no modal is up, so leaving the window with no focus widget would leave the + // hotkeys dead (REQ-UI-HOTKEYS). + if (newWidget != m_gameWorldView && !QApplication::activeModalWidget() && !m_modalLayer->isActive()) { m_gameWorldView->setFocus(); diff --git a/src/ui/ModalLayer.cpp b/src/ui/ModalLayer.cpp index ed5fe93..98441ac 100644 --- a/src/ui/ModalLayer.cpp +++ b/src/ui/ModalLayer.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -40,6 +41,11 @@ int ModalLayer::execute(ModalDialog& content, const QRect& anchorRect) // 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); @@ -52,7 +58,7 @@ int ModalLayer::execute(ModalDialog& content, const QRect& anchorRect) : (m_stack.empty() ? rect() : m_stack.back().host->geometry()); - m_stack.push_back(HostedModal{ &content, host }); + 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 @@ -90,15 +96,24 @@ int ModalLayer::execute(ModalDialog& content, const QRect& anchorRect) 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(); } diff --git a/src/ui/ModalLayer.h b/src/ui/ModalLayer.h index c9dd4b0..5f50f67 100644 --- a/src/ui/ModalLayer.h +++ b/src/ui/ModalLayer.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -65,6 +66,12 @@ private: { 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