diff --git a/src/ui/BlueprintSelectionDialog.cpp b/src/ui/BlueprintSelectionDialog.cpp index 3f21aaa..9df17b7 100644 --- a/src/ui/BlueprintSelectionDialog.cpp +++ b/src/ui/BlueprintSelectionDialog.cpp @@ -28,7 +28,6 @@ #include "Blueprint.h" #include "BlueprintLibrary.h" -#include "DialogDismiss.h" #include "IconCaption.h" #include "ItemIconCache.h" @@ -150,54 +149,28 @@ namespace BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library, ItemIconCache* itemIcons, QWidget* parent) - : QDialog(parent) + : ModalDialog(parent) , m_library(library) , m_itemIcons(itemIcons) { - setWindowTitle(tr("Blueprints")); - setModal(true); - // Frameless: the dialog draws its own header row, so an OS title bar would only - // repeat it (REQ-UI-BLUEPRINT-DIALOG). Square corners rather than rounded ones -- - // rounding a top-level window needs a translucent background, which is unreliable - // on Windows. The border matches the build bar and the selection panel. - setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint); - setAttribute(Qt::WA_StyledBackground, true); - setStyleSheet(QStringLiteral( - "BlueprintSelectionDialog { background-color: palette(window);" - " border: 1px solid palette(mid); }")); - QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx); mainLayout->setSpacing(kSpacingPx); - QHBoxLayout* headerLayout = new QHBoxLayout(); - headerLayout->setSpacing(kSpacingPx); - - QLabel* titleLabel = new QLabel(tr("Blueprints"), this); - QFont headerFont = titleLabel->font(); - headerFont.setBold(true); - titleLabel->setFont(headerFont); - headerLayout->addWidget(titleLabel); + QHBoxLayout* headerLayout = addHeader(mainLayout, tr("Blueprints"), true); // Dimmed and bold, the treatment the build button hotkey badges use, so the // shortcut reads as a reminder rather than a second title (REQ-UI-BUILD-COST). + // Inserted right after the title, left of the stretch the header ends with. QLabel* hotkeyBadge = new QLabel(tr("Ctrl+V"), this); - hotkeyBadge->setFont(headerFont); + QFont badgeFont = hotkeyBadge->font(); + badgeFont.setBold(true); + hotkeyBadge->setFont(badgeFont); QPalette badgePalette = hotkeyBadge->palette(); badgePalette.setColor(hotkeyBadge->foregroundRole(), palette().color(QPalette::Disabled, QPalette::WindowText)); hotkeyBadge->setPalette(badgePalette); - headerLayout->addWidget(hotkeyBadge); - - headerLayout->addStretch(); - - QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this); - closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx); - closeButton->setToolTip(tr("Close")); - connect(closeButton, &QPushButton::clicked, this, &QDialog::reject); - headerLayout->addWidget(closeButton); - - mainLayout->addLayout(headerLayout); + headerLayout->insertWidget(1, hotkeyBadge); QScrollArea* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); @@ -223,13 +196,11 @@ BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library, setFixedSize(gridWidth + style()->pixelMetric(QStyle::PM_ScrollBarExtent) + 2 * kSpacingPx, gridHeight + kSmallButtonSizePx + 3 * kSpacingPx); +} - // A frameless dialog does not get Qt's automatic centering on its parent. - if (parent) - { - const QRect parentRect = parent->window()->geometry(); - move(parentRect.center() - QPoint(width() / 2, height() / 2)); - } +bool BlueprintSelectionDialog::isDismissible() const +{ + return true; } std::optional BlueprintSelectionDialog::getChosenIndex() const @@ -237,19 +208,6 @@ std::optional BlueprintSelectionDialog::getChosenIndex() const return m_chosenIndex; } -void BlueprintSelectionDialog::keyPressEvent(QKeyEvent* event) -{ - // Q closes with no card picked, exactly as the close button does. A build mode - // running underneath is left alone: the dialog took the key, not the world - // (REQ-UI-DIALOG-DISMISS, REQ-UI-BLUEPRINT-DIALOG). - if (isDialogDismissKey(*event)) - { - reject(); - return; - } - QDialog::keyPressEvent(event); -} - void BlueprintSelectionDialog::rebuildGrid() { // deleteLater, not delete: this runs from a delete button's own clicked signal, and diff --git a/src/ui/BlueprintSelectionDialog.h b/src/ui/BlueprintSelectionDialog.h index d53041b..a8c6ce5 100644 --- a/src/ui/BlueprintSelectionDialog.h +++ b/src/ui/BlueprintSelectionDialog.h @@ -2,23 +2,23 @@ #include -#include +#include "ModalDialog.h" class BlueprintLibrary; class ItemIconCache; class QGridLayout; class QWidget; -// The blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG): a frameless modal panel -// showing every saved blueprint as a card in a scrolling two-column grid. The caller -// pauses the game and raises the dim overlay while it is open. +// The blueprint selection dialog (REQ-UI-BLUEPRINT-DIALOG): a modal panel showing every +// saved blueprint as a card in a scrolling two-column grid. The caller pauses the game +// while it is open. // // Clicking a card accepts the dialog and reports that blueprint's index; the caller // enters placement mode afterwards, so the dialog is already closed by then // (REQ-UI-BLUEPRINT-CARD). Deleting acts on the library immediately and leaves the -// dialog open (REQ-UI-BLUEPRINT-DELETE). Escape, Q, and the close button dismiss it -// with no other effect (REQ-UI-DIALOG-DISMISS). -class BlueprintSelectionDialog : public QDialog +// dialog open (REQ-UI-BLUEPRINT-DELETE). Escape, Q, the close button, and a click +// outside dismiss it with no other effect (REQ-UI-DIALOG-DISMISS). +class BlueprintSelectionDialog : public ModalDialog { Q_OBJECT @@ -29,8 +29,7 @@ public: std::optional getChosenIndex() const; -protected: - void keyPressEvent(QKeyEvent* event) override; + bool isDismissible() const override; private: void rebuildGrid(); diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 6e7a555..2a258ad 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -5,7 +5,8 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/VisualsConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.h - ${CMAKE_CURRENT_SOURCE_DIR}/ModalDimOverlay.h + ${CMAKE_CURRENT_SOURCE_DIR}/ModalDialog.h + ${CMAKE_CURRENT_SOURCE_DIR}/ModalLayer.h ${CMAKE_CURRENT_SOURCE_DIR}/ModalPauseScope.h ${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.h ${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.h @@ -18,7 +19,6 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.h ${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h ${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h - ${CMAKE_CURRENT_SOURCE_DIR}/DialogDismiss.h ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h ${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h ${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h @@ -44,7 +44,8 @@ SET(SRCS ${SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/ModalDimOverlay.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ModalDialog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ModalLayer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.cpp diff --git a/src/ui/DialogDismiss.h b/src/ui/DialogDismiss.h deleted file mode 100644 index 16d9c8c..0000000 --- a/src/ui/DialogDismiss.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -// REQ-UI-DIALOG-DISMISS: Q dismisses an open dialog, beside the Escape that QDialog -// already handles. The key is spelled here rather than in each dialog's key handler, so -// the dialogs that take it cannot drift apart from one another. -// -// Ctrl must not be held, matching how the game world's table separates a chord from the -// bare key (resolveKeyAction in lib/core/ControlAction.cpp). This deliberately stays out -// of that table: the table answers what an input does in the player's current situation, -// and a dialog has no situation -- it holds focus and takes the key whatever the world -// beneath it is doing. -inline bool isDialogDismissKey(const QKeyEvent& event) -{ - return event.key() == Qt::Key_Q - && (event.modifiers() & Qt::ControlModifier) == 0; -} diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index 3c6f855..f6c9a4d 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -35,6 +35,7 @@ #include "ShipLayoutDialog.h" #include "BuildingIconCache.h" #include "ItemIconCache.h" +#include "ModalLayer.h" #include "ModalPauseScope.h" #include "Simulation.h" #include "Tick.h" @@ -98,14 +99,18 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, // after the view for the same stacking reason as the panels above it. m_controlsPanel = new ControlsPanel(m_gameWorldView, this); - // Created last so it stacks above the other children; covers the whole window and - // dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM). - m_dimOverlay = new ModalDimOverlay(m_visuals.overlays.modalDim, this); + // Created last so it stacks above the other children; covers the whole window, + // dims the game behind every modal, and is what those modals are drawn on + // (REQ-UI-MODAL-DIM, REQ-UI-MODAL-CHROME). + m_modalLayer = new ModalLayer(m_visuals.overlays.modalDim, this); m_gameWorldView->setFocus(); connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) { - if (newWidget && newWidget != m_gameWorldView && !QApplication::activeModalWidget()) + // 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() + && !m_modalLayer->isActive()) { m_gameWorldView->setFocus(); } @@ -145,6 +150,15 @@ void MainWindow::resizeEvent(QResizeEvent* event) void MainWindow::closeEvent(QCloseEvent* event) { + // A modal on the layer runs a nested event loop over widgets this window owns, so + // the window must outlive it. A modal window used to block the close outright; this + // does the same for a modal that is only a widget (REQ-UI-MODAL-CHROME). + if (m_modalLayer->isActive()) + { + event->ignore(); + return; + } + const QString path = QCoreApplication::applicationDirPath() + "/ship_layouts.toml"; QFile file(path); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) @@ -173,7 +187,7 @@ void MainWindow::layoutPanels() const QRect worldRect(0, headerH, totalW, totalH - headerH); m_headerBar->setGeometry(0, 0, totalW, headerH); m_gameWorldView->setGeometry(worldRect); - m_dimOverlay->setGeometry(0, 0, totalW, totalH); + m_modalLayer->setGeometry(0, 0, totalW, totalH); // The floating widgets are placed in one ordered pass, each into the space the // earlier ones have not taken (FloatingPanel.h). The order is the priority the @@ -223,10 +237,9 @@ void MainWindow::handleEvent(std::shared_ptrchoices, m_sim->getConfig().recipes, - m_itemIcons.get(), m_buildingIcons.get(), this); - dialog.exec(); + m_itemIcons.get(), m_buildingIcons.get(), m_modalLayer); + m_modalLayer->execute(dialog); // The command goes out unconditionally because the dialog cannot be dismissed: it // returns only once an option was clicked, so the index always names that option @@ -244,7 +257,7 @@ void MainWindow::handleEvent(std::shared_ptr /*e { ModalPauseScope pause(*m_gameWorldView); - ModalDimScope dim(*m_dimOverlay); + ModalLayerHold dim(*m_modalLayer); QMessageBox box(this); box.setWindowTitle(tr("Paused")); QPushButton* continueBtn = box.addButton(tr("Continue"), QMessageBox::AcceptRole); @@ -289,7 +302,7 @@ std::optional MainWindow::reloadConfig() GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir); VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml"); m_visuals = std::move(newVisuals); - m_dimOverlay->setDimColor(m_visuals.overlays.modalDim); + m_modalLayer->setDimColor(m_visuals.overlays.modalDim); // The composed item squares carry the colors they were painted with, so they // are dropped for the new ones to take effect (REQ-UI-ITEM-ICON). m_itemIcons->clearPixmapCache(); @@ -318,16 +331,15 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId, } } - ModalDimScope dim(*m_dimOverlay); ShipLayoutDialog dialog(&m_sim->getConfig(), schematicId, currentLayout, m_layoutBlueprints, std::move(unlockedModuleIds), m_gameWorldView->isDebugDrawEnabled(), - m_itemIcons.get(), this); + m_itemIcons.get(), m_modalLayer); // Opened from the panel's "Configure" button (REQ-MOD-UI-PREVIEW) or straight after // a schematic change (REQ-MOD-UI-AUTO-DIALOG), so it opens on the panel either way. - placeOnSelectionPanel(dialog); - if (dialog.exec() == QDialog::Accepted && dialog.getResult().has_value()) + if (m_modalLayer->execute(dialog, getSelectionPanelAnchor()) == QDialog::Accepted + && dialog.getResult().has_value()) { std::shared_ptr command = std::make_shared(); @@ -338,43 +350,18 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId, } } -void MainWindow::placeOnSelectionPanel(QDialog& dialog) const +QRect MainWindow::getSelectionPanelAnchor() const { // The panel is up whenever one of these modals opens -- they are opened from its own // controls, and it is shown whenever anything is selected (REQ-UI-EMPTY-SELECTION). - // Were it not, there would be no rectangle to center on and Qt's own centering on - // this window stands. - if (!m_selectionPanel->isVisible()) { return; } - - // The dialog has never been shown, so it is still at its default size until its - // layout has run; centering it before that would use the wrong extent. - dialog.adjustSize(); - const QSize dialogSize = dialog.size(); + // Were it not, there would be no rectangle to center on and the layer's own centering + // stands (REQ-UI-PANEL-MODAL). + if (!m_selectionPanel->isVisible()) { return QRect(); } // The panel's live geometry, so a panel the player has dragged - // (REQ-UI-SELECTION-PANEL-DRAG) carries the modal with it. - const QRect panelRect(m_selectionPanel->mapToGlobal(QPoint(0, 0)), - m_selectionPanel->size()); - const QRect windowRect(mapToGlobal(QPoint(0, 0)), size()); - - QPoint topLeft(panelRect.center().x() - dialogSize.width() / 2, - panelRect.center().y() - dialogSize.height() / 2); - - // Pushed back inside the window, never resized to fit (REQ-UI-PANEL-MODAL). The far - // edge is clamped first and the near edge second, which is what aligns a dialog too - // large for the window with the window's top-left corner rather than pushing it off - // the opposite edge. - topLeft.setX(qMax(windowRect.left(), - qMin(topLeft.x(), windowRect.right() - dialogSize.width() + 1))); - topLeft.setY(qMax(windowRect.top(), - qMin(topLeft.y(), windowRect.bottom() - dialogSize.height() + 1))); - - // Positions the dialog's frame, whose size is not known until it is first shown, so - // the result sits low by the title bar height against a true center -- measuring it - // would mean showing the dialog at the wrong place first. The move also marks the - // dialog as positioned, which is what stops QDialog from centering it on this window - // when it is shown. - dialog.move(topLeft); + // (REQ-UI-SELECTION-PANEL-DRAG) carries the modal with it. Panel and layer are both + // children of this window, so the panel's geometry needs no mapping. + return m_selectionPanel->geometry(); } void MainWindow::handleEvent(std::shared_ptr event) @@ -427,7 +414,7 @@ void MainWindow::handleEvent(std::shared_ptrtype : s->type; // Captured as a copy: a queued command may drain during the modal dialog's @@ -443,9 +430,9 @@ void MainWindow::handleEvent(std::shared_ptrexecute(dialog, getSelectionPanelAnchor()) == QDialog::Accepted + && dialog.getChosenId().has_value()) { std::shared_ptr command = std::make_shared(); command->id = event->buildingId; @@ -487,7 +474,7 @@ void MainWindow::handleEvent(std::shared_ptr // over to, so the dim never blinks off and the simulation is not resumed in between // (REQ-UI-MODAL-DIM). ModalPauseScope pause(*m_gameWorldView); - ModalDimScope dim(*m_dimOverlay); + ModalLayerHold dim(*m_modalLayer); bool ok = false; const QString name = QInputDialog::getText( @@ -503,14 +490,15 @@ void MainWindow::handleEvent(std::shared_ptr void MainWindow::handleEvent(std::shared_ptr /*event*/) { ModalPauseScope pause(*m_gameWorldView); - ModalDimScope dim(*m_dimOverlay); showBlueprintSelectionDialog(); } void MainWindow::showBlueprintSelectionDialog() { - BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), this); - if (dialog.exec() == QDialog::Accepted && dialog.getChosenIndex().has_value()) + BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), + m_modalLayer); + if (m_modalLayer->execute(dialog) == QDialog::Accepted + && dialog.getChosenIndex().has_value()) { // Entered after the dialog has closed, which is the order REQ-UI-BLUEPRINT-CARD // describes: clicking a card closes the dialog and enters placement mode. @@ -525,7 +513,7 @@ void MainWindow::handleEvent(std::shared_ptr /*event*/) const int minutes = totalSeconds / 60; const int seconds = totalSeconds % 60; - ModalDimScope dim(*m_dimOverlay); + ModalLayerHold dim(*m_modalLayer); QMessageBox box(this); box.setWindowTitle(tr("Game Over")); box.setText(tr("HQ destroyed!\nSurvival time: %1:%2") @@ -562,7 +550,7 @@ void MainWindow::handleEvent(std::shared_ptr /*event*/) const int minutes = totalSeconds / 60; const int seconds = totalSeconds % 60; - ModalDimScope dim(*m_dimOverlay); + ModalLayerHold dim(*m_modalLayer); QMessageBox box(this); box.setWindowTitle(tr("Won!")); box.setText(tr("You collected all artifacts!\nSurvival time: %1:%2") diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index de74473..73a454e 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -16,7 +16,7 @@ #include "GameConfig.h" #include "GameOverEvent.h" #include "LayoutDialogRequestedEvent.h" -#include "ModalDimOverlay.h" +#include "ModalLayer.h" #include "WinEvent.h" #include "RecipeSelectionRequestedEvent.h" #include "SchematicChoicesAvailableEvent.h" @@ -36,7 +36,6 @@ class BlueprintLibrary; class BuildingIconCache; class ItemIconCache; class QCloseEvent; -class QDialog; class QResizeEvent; class MainWindow : public QWidget, @@ -84,15 +83,15 @@ private: const std::string& schematicId, const ShipLayoutConfig& currentLayout); - // Centers a modal opened from the selection panel on that panel, kept inside this - // window (REQ-UI-PANEL-MODAL). Called on the constructed dialog before exec(), and - // only for the two modals the panel opens. - void placeOnSelectionPanel(QDialog& dialog) const; + // The rectangle a modal opened from the selection panel is centered on, in this + // window's coordinates, or a null rect when the panel is not up (REQ-UI-PANEL-MODAL). + QRect getSelectionPanelAnchor() const; // Runs the blueprint selection dialog and enters placement mode for whatever the - // player picked (REQ-UI-BLUEPRINT-DIALOG). Holds no pause or dim scope of its own: - // both callers already hold theirs, which is what keeps the dim continuous when a - // confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM). + // player picked (REQ-UI-BLUEPRINT-DIALOG). Holds no pause scope of its own: both + // callers already hold theirs, and the save path also holds the layer, which is what + // keeps the dim continuous when a confirmed save hands straight over to this dialog + // (REQ-UI-MODAL-DIM). void showBlueprintSelectionDialog(); // Places the widgets floating over the game world view, in one ordered pass // (FloatingPanel.h). Runs on a resize and on every FloatingLayoutInvalidatedEvent. @@ -116,7 +115,7 @@ private: // The saved blueprints themselves; they have no widget of their own any more and // are reached through the two modal dialogs (REQ-UI-BLUEPRINT-DIALOG). std::unique_ptr m_blueprintLibrary; - ModalDimOverlay* m_dimOverlay = nullptr; + ModalLayer* m_modalLayer = nullptr; std::vector m_layoutBlueprints; std::shared_ptr m_replay; // non-null => view-only playback diff --git a/src/ui/ModalDialog.cpp b/src/ui/ModalDialog.cpp new file mode 100644 index 0000000..dac3255 --- /dev/null +++ b/src/ui/ModalDialog.cpp @@ -0,0 +1,97 @@ +#include "ModalDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + // The header's own metrics, matching the row the blueprint selection dialog drew + // before this class existed (REQ-UI-BLUEPRINT-DIALOG). + const int kSpacingPx = 8; + const int kSmallButtonSizePx = 22; + const QChar kCrossGlyph(0x00D7); // U+00D7 MULTIPLICATION SIGN + + // Q dismisses an open dialog, beside the Escape that QDialog already handles + // (REQ-UI-DIALOG-DISMISS). Ctrl must not be held, matching how the game world's + // table separates a chord from the bare key (resolveKeyAction in + // lib/core/ControlAction.cpp). This deliberately stays out of that table: the table + // answers what an input does in the player's current situation, and a dialog has no + // situation -- it holds focus and takes the key whatever the world beneath it is + // doing. + bool isDismissKey(const QKeyEvent& event) + { + return event.key() == Qt::Key_Q + && (event.modifiers() & Qt::ControlModifier) == 0; + } +} + +ModalDialog::ModalDialog(QWidget* parent) + : QDialog(parent) +{ + // An ordinary child widget rather than a window: the layer places it and the dim + // behind it is the same widget's paint (REQ-UI-MODAL-CHROME). Square corners rather + // than rounded ones -- rounding needs a translucent background, which is unreliable + // on Windows. The border matches the build bar and the selection panel. + setWindowFlags(Qt::Widget); + setAttribute(Qt::WA_StyledBackground, true); + // A type selector, so it reaches every subclass but none of the child widgets + // inside them, which draw themselves. + setStyleSheet(QStringLiteral( + "ModalDialog { background-color: palette(window);" + " border: 1px solid palette(mid); }")); +} + +bool ModalDialog::isDismissible() const +{ + return false; +} + +void ModalDialog::requestDismiss() +{ + reject(); +} + +QHBoxLayout* ModalDialog::addHeader(QBoxLayout* mainLayout, const QString& title, + bool withCloseButton) +{ + QHBoxLayout* headerLayout = new QHBoxLayout(); + headerLayout->setSpacing(kSpacingPx); + + QLabel* titleLabel = new QLabel(title, this); + QFont headerFont = titleLabel->font(); + headerFont.setBold(true); + titleLabel->setFont(headerFont); + headerLayout->addWidget(titleLabel); + + // The stretch is added before the close button so a subclass inserting after the + // title lands left of it, where the blueprint dialog's hotkey badge belongs. + headerLayout->addStretch(); + + if (withCloseButton) + { + QPushButton* closeButton = new QPushButton(QString(kCrossGlyph), this); + closeButton->setFixedSize(kSmallButtonSizePx, kSmallButtonSizePx); + closeButton->setToolTip(tr("Close")); + connect(closeButton, &QPushButton::clicked, this, &QDialog::reject); + headerLayout->addWidget(closeButton); + } + + mainLayout->insertLayout(0, headerLayout); + return headerLayout; +} + +void ModalDialog::keyPressEvent(QKeyEvent* event) +{ + if (isDismissible() && isDismissKey(*event)) + { + requestDismiss(); + return; + } + QDialog::keyPressEvent(event); +} diff --git a/src/ui/ModalDialog.h b/src/ui/ModalDialog.h new file mode 100644 index 0000000..3c981ab --- /dev/null +++ b/src/ui/ModalDialog.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +class QBoxLayout; +class QHBoxLayout; +class QKeyEvent; +class QString; + +// Base of every modal the player meets while playing (REQ-UI-MODAL-CHROME). A modal is +// not an operating system window here: it is an ordinary child widget, hosted and placed +// by the ModalLayer that also paints the dim it sits on, so it has no title bar, no +// window border, and no window-manager close. What a window used to supply, this class +// supplies instead -- the panel background, the drawn header, and the dismissal gestures. +// +// It stays a QDialog for accept()/reject()/result() and the Escape handling built into +// them; only the window-ness is dropped (Qt::Widget flags). Nothing calls exec() on it: +// ModalLayer::execute() runs the modal loop, so that the layer knows what is open. +class ModalDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ModalDialog(QWidget* parent = nullptr); + + // Whether Q and a click outside this dialog dismiss it (REQ-UI-DIALOG-DISMISS). One + // predicate for both gestures because they reach exactly the same dialogs. The + // default refuses both, so a dialog takes the gestures only by saying so: the two + // name dialogs must let Q through as a character, and the schematic choice dialog + // has no way out but choosing (REQ-DEF-SCHEMATIC-DROP). + virtual bool isDismissible() const; + +public slots: + // What a dismissal does, whichever gesture asked for it. Cancelling is the default; + // a dialog with modes of its own overrides this to back out of them one at a time + // (ShipLayoutDialog, REQ-UI-DIALOG-DISMISS). + virtual void requestDismiss(); + +protected: + // Adds the drawn header row -- the title, and a close button at the far right when + // asked for -- as the first row of mainLayout, and returns it so a subclass can + // insert its own widgets after the title (REQ-UI-MODAL-CHROME). The close button + // rejects the dialog, which is what the window-manager close used to do. + QHBoxLayout* addHeader(QBoxLayout* mainLayout, const QString& title, + bool withCloseButton); + + void keyPressEvent(QKeyEvent* event) override; +}; diff --git a/src/ui/ModalDimOverlay.cpp b/src/ui/ModalDimOverlay.cpp deleted file mode 100644 index 31c2c2c..0000000 --- a/src/ui/ModalDimOverlay.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "ModalDimOverlay.h" - -#include - -ModalDimOverlay::ModalDimOverlay(const QColor& dimColor, QWidget* parent) - : QWidget(parent) - , m_dimColor(dimColor) -{ - setAttribute(Qt::WA_TransparentForMouseEvents, true); - hide(); -} - -void ModalDimOverlay::pushModal() -{ - if (m_modalDepth++ == 0) - { - raise(); - show(); - // Force an immediate synchronous paint so the scrim is visible before the - // caller enters a blocking dialog exec() (no undimmed frame flashes through). - repaint(); - } -} - -void ModalDimOverlay::popModal() -{ - if (m_modalDepth > 0 && --m_modalDepth == 0) - { - hide(); - } -} - -void ModalDimOverlay::setDimColor(const QColor& dimColor) -{ - m_dimColor = dimColor; - if (isVisible()) - { - update(); - } -} - -void ModalDimOverlay::paintEvent(QPaintEvent* /*event*/) -{ - QPainter painter(this); - painter.fillRect(rect(), m_dimColor); -} diff --git a/src/ui/ModalDimOverlay.h b/src/ui/ModalDimOverlay.h deleted file mode 100644 index 591af52..0000000 --- a/src/ui/ModalDimOverlay.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include -#include - -class QPaintEvent; - -// A window-wide, semi-transparent scrim drawn over the entire game window while a -// modal dialog or menu is open, behind that modal (REQ-UI-MODAL-DIM). It is a child -// of the main window covering its full rect and is transparent to mouse events, so it -// only dims the game presentation and never intercepts input. -// -// Visibility is reference-counted via pushModal()/popModal() so that a single dim is -// shown across nested or back-to-back modals (e.g. the recipe selection dialog that -// immediately opens the layout dialog) rather than flickering or stacking overlays. -class ModalDimOverlay : public QWidget -{ - Q_OBJECT - -public: - ModalDimOverlay(const QColor& dimColor, QWidget* parent); - - // Raise + show on the first active modal; hide when the last one closes. - void pushModal(); - void popModal(); - - // 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; - -private: - QColor m_dimColor; - int m_modalDepth = 0; -}; - -// RAII guard: shows the dim overlay for the duration of a scope (typically around a -// blocking dialog exec()) and hides it (via reference count) on scope exit. -class ModalDimScope -{ -public: - explicit ModalDimScope(ModalDimOverlay& overlay) - : m_overlay(overlay) - { - m_overlay.pushModal(); - } - - ~ModalDimScope() - { - m_overlay.popModal(); - } - - ModalDimScope(const ModalDimScope&) = delete; - ModalDimScope& operator=(const ModalDimScope&) = delete; - -private: - ModalDimOverlay& m_overlay; -}; diff --git a/src/ui/ModalLayer.cpp b/src/ui/ModalLayer.cpp new file mode 100644 index 0000000..df3a472 --- /dev/null +++ b/src/ui/ModalLayer.cpp @@ -0,0 +1,140 @@ +#include "ModalLayer.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#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(); + } +} diff --git a/src/ui/ModalLayer.h b/src/ui/ModalLayer.h new file mode 100644 index 0000000..1c05edd --- /dev/null +++ b/src/ui/ModalLayer.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include +#include +#include + +class ModalDialog; +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: + 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 layer + // itself (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; + +private: + // 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(); + + QColor m_dimColor; + 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; +}; diff --git a/src/ui/ModalPauseScope.h b/src/ui/ModalPauseScope.h index c3085fc..b3a8ba8 100644 --- a/src/ui/ModalPauseScope.h +++ b/src/ui/ModalPauseScope.h @@ -7,7 +7,7 @@ // exit it restores the snapshotted speed and rebases the render frame timer, so the // wall time the player spent in the dialog is not converted into simulation ticks. // -// Pairs with ModalDimScope, which the same call sites use for the dim overlay. +// Pairs with ModalLayer, which the same call sites use to show the modal itself. // // Two escape hatches for the paths that must not simply restore at scope exit: // restore() — restore now instead of at scope exit, for when more work has to run diff --git a/src/ui/RecipeSelectionDialog.cpp b/src/ui/RecipeSelectionDialog.cpp index e647d80..31c70a6 100644 --- a/src/ui/RecipeSelectionDialog.cpp +++ b/src/ui/RecipeSelectionDialog.cpp @@ -11,7 +11,6 @@ #include "Building.h" #include "BuildingType.h" -#include "DialogDismiss.h" #include "DisplayName.h" #include "GameConfig.h" #include "OptionButton.h" @@ -109,11 +108,8 @@ RecipeSelectionDialog::RecipeSelectionDialog( const std::vector& options, const QString& title, ItemIconCache* itemIcons, BuildingIconCache* buildingIcons, QWidget* parent) - : QDialog(parent) + : ModalDialog(parent) { - setWindowTitle(title); - setModal(true); - QVBoxLayout* mainLayout = new QVBoxLayout(this); // One vertical column of buttons, each stating what it makes (REQ-UI-SELECT-OPTIONS). @@ -177,6 +173,16 @@ RecipeSelectionDialog::RecipeSelectionDialog( listLayout->addStretch(1); scrollArea->setWidget(list); + + // Added last so the header sits above a list whose height is already settled; the + // modal draws its own title and close button, having no window frame to carry them + // (REQ-UI-MODAL-CHROME). + addHeader(mainLayout, title, true); +} + +bool RecipeSelectionDialog::isDismissible() const +{ + return true; } std::optional RecipeSelectionDialog::getChosenId() const @@ -184,18 +190,6 @@ std::optional RecipeSelectionDialog::getChosenId() const return m_chosenId; } -void RecipeSelectionDialog::keyPressEvent(QKeyEvent* event) -{ - // Q dismisses, leaving the recipe as it was -- the same nothing that Escape and the - // close button do, since no option was clicked (REQ-UI-DIALOG-DISMISS). - if (isDialogDismissKey(*event)) - { - reject(); - return; - } - QDialog::keyPressEvent(event); -} - void RecipeSelectionDialog::onOptionClicked(int index) { if (index >= 0 && index < static_cast(m_optionIds.size())) diff --git a/src/ui/RecipeSelectionDialog.h b/src/ui/RecipeSelectionDialog.h index 1dd2a3a..7c3b549 100644 --- a/src/ui/RecipeSelectionDialog.h +++ b/src/ui/RecipeSelectionDialog.h @@ -4,10 +4,10 @@ #include #include -#include #include #include "BuildingType.h" +#include "ModalDialog.h" #include "RecipeLineRow.h" struct GameConfig; @@ -39,9 +39,10 @@ std::vector buildRecipeSelectionOptions( BuildingType type, Simulation& sim, const GameConfig& config); // Modal dialog listing the options in one vertical column (REQ-UI-SELECT-OPTIONS). The -// game is paused by the caller while it is open. Clicking an option selects it -// and closes the dialog; dismissing it (close/Esc/Q) leaves no choice. -class RecipeSelectionDialog : public QDialog +// game is paused by the caller while it is open. Clicking an option selects it and closes +// the dialog; dismissing it (close button, Escape, Q, or a click outside) leaves no +// choice (REQ-UI-DIALOG-DISMISS). +class RecipeSelectionDialog : public ModalDialog { Q_OBJECT @@ -53,8 +54,7 @@ public: std::optional getChosenId() const; -protected: - void keyPressEvent(QKeyEvent* event) override; + bool isDismissible() const override; private: void onOptionClicked(int index); diff --git a/src/ui/SchematicChoiceDialog.cpp b/src/ui/SchematicChoiceDialog.cpp index 5931c1a..374be5b 100644 --- a/src/ui/SchematicChoiceDialog.cpp +++ b/src/ui/SchematicChoiceDialog.cpp @@ -44,15 +44,14 @@ SchematicChoiceDialog::SchematicChoiceDialog( const RecipesConfig& recipes, ItemIconCache* itemIcons, BuildingIconCache* buildingIcons, QWidget* parent) - : QDialog(parent) + : ModalDialog(parent) , m_chosenIndex(0) { - setWindowTitle(tr("Schematic Drop")); - setWindowFlags(windowFlags() & ~Qt::WindowCloseButtonHint); - setModal(true); - QVBoxLayout* mainLayout = new QVBoxLayout(this); + // Its own title line, and the only one: a modal draws its title rather than wearing + // one (REQ-UI-MODAL-CHROME), and this dialog carries no close button because there + // is no way out but choosing (REQ-DEF-SCHEMATIC-DROP). QLabel* titleLabel = new QLabel(tr("Choose a schematic to unlock:"), this); QFont titleFont = titleLabel->font(); titleFont.setPointSize(titleFont.pointSize() + 2); diff --git a/src/ui/SchematicChoiceDialog.h b/src/ui/SchematicChoiceDialog.h index 386b71c..d6c0b9a 100644 --- a/src/ui/SchematicChoiceDialog.h +++ b/src/ui/SchematicChoiceDialog.h @@ -2,18 +2,19 @@ #include -#include - +#include "ModalDialog.h" #include "SchematicChoiceOption.h" struct RecipesConfig; class BuildingIconCache; class ItemIconCache; -// The drop's choice dialog (REQ-DEF-SCHEMATIC-DROP). Unlike every other dialog it -// cannot be dismissed: clicking an option is the only way out, so getChosenIndex() -// always names an option the player picked, and exec() only ever returns Accepted. -class SchematicChoiceDialog : public QDialog +// The drop's choice dialog (REQ-DEF-SCHEMATIC-DROP). Unlike every other dialog it cannot +// be dismissed: clicking an option is the only way out, so getChosenIndex() always names +// an option the player picked, and it only ever finishes Accepted. It inherits the base's +// refusal of Q and of a click outside (ModalDialog::isDismissible) and adds the refusal +// of Escape below. +class SchematicChoiceDialog : public ModalDialog { Q_OBJECT @@ -30,9 +31,8 @@ public: public slots: // Refuses the dismissal (REQ-DEF-SCHEMATIC-DROP): the drop is a reward the player // has earned, and leaving without choosing would either forfeit it or award the - // option that happens to be first. Escape, Alt+F4, and the window manager's close - // all funnel through QDialog::reject(), so declining it here turns away every one of - // them at once rather than swallowing keys one at a time. + // option that happens to be first. Escape funnels through QDialog::reject(), so + // declining it here turns it away without swallowing keys one at a time. void reject() override; private: diff --git a/src/ui/ShipLayoutDialog.cpp b/src/ui/ShipLayoutDialog.cpp index e25d931..fdae58d 100644 --- a/src/ui/ShipLayoutDialog.cpp +++ b/src/ui/ShipLayoutDialog.cpp @@ -4,7 +4,6 @@ #include #include -#include "DialogDismiss.h" #include "DisplayName.h" #include "OptionButton.h" #include "ProductionRules.h" @@ -421,7 +420,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, bool debugDraw, ItemIconCache* itemIcons, QWidget* parent) - : QDialog(parent) + : ModalDialog(parent) , m_config(config) , m_itemIcons(itemIcons) , m_shipId(shipId) @@ -435,9 +434,6 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, , m_statsPanel(nullptr) , m_debugDraw(debugDraw) { - setWindowTitle(tr("Configure Ship Layout")); - setModal(true); - // Find the ship's layout grid. const ShipDef* shipDef = config->ships.findShipDef(shipId); if (shipDef) @@ -473,6 +469,10 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config, // --- UI layout --- QVBoxLayout* outerLayout = new QVBoxLayout(this); + // The dialog's own title line; it carries no close button, having Confirm and Cancel + // of its own at the bottom (REQ-UI-MODAL-CHROME, REQ-MOD-UI-DIALOG). + addHeader(outerLayout, tr("Configure Ship Layout"), false); + // Top: grid widget. LayoutGridWidget* gridW = new LayoutGridWidget(this, this); gridW->setGridData(&m_grid, m_rows, m_cols, &m_placedModules, m_config); @@ -682,22 +682,27 @@ void ShipLayoutDialog::keyPressEvent(QKeyEvent* event) } updateGridWidget(); } - else if (isDialogDismissKey(*event)) - { - // Q backs out one level per press, as it does in the game world: the module - // being placed, then remove mode, then the dialog itself, which discards the - // session (REQ-UI-DIALOG-DISMISS). Both mode exits go through the handler the - // button uses, so the key and the button can never leave different state behind. - if (m_activeModuleIndex.has_value()) { onModuleButtonClicked(*m_activeModuleIndex); } - else if (m_removeMode) { onRemoveButtonClicked(); } - else { onCancel(); } - } else { - QDialog::keyPressEvent(event); + // Q among them, which the base turns into requestDismiss(). + ModalDialog::keyPressEvent(event); } } +bool ShipLayoutDialog::isDismissible() const +{ + return true; +} + +void ShipLayoutDialog::requestDismiss() +{ + // Both mode exits go through the handler the buttons use, so a dismissal and a + // button can never leave different state behind (REQ-MOD-PLACEMENT, REQ-MOD-REMOVE). + if (m_activeModuleIndex.has_value()) { onModuleButtonClicked(*m_activeModuleIndex); } + else if (m_removeMode) { onRemoveButtonClicked(); } + else { onCancel(); } +} + void ShipLayoutDialog::onModuleButtonClicked(int index) { if (m_activeModuleIndex == index) diff --git a/src/ui/ShipLayoutDialog.h b/src/ui/ShipLayoutDialog.h index e9225e8..6bd7835 100644 --- a/src/ui/ShipLayoutDialog.h +++ b/src/ui/ShipLayoutDialog.h @@ -6,10 +6,10 @@ #include #include -#include #include #include "GameConfig.h" +#include "ModalDialog.h" #include "Rotation.h" #include "ShipLayout.h" #include "ShipLayoutBlueprint.h" @@ -20,7 +20,7 @@ class RecipeLineRow; class SectionBox; class ShipStatsPanel; -class ShipLayoutDialog : public QDialog +class ShipLayoutDialog : public ModalDialog { Q_OBJECT @@ -38,6 +38,15 @@ public: std::optional getResult() const; + bool isDismissible() const override; + +public slots: + // Backs out one level per dismissal, as Q does in the game world: the module being + // placed, then remove mode, then the dialog itself, which discards the session + // (REQ-UI-DIALOG-DISMISS). So a single press or click never both leaves a mode and + // throws the session away. + void requestDismiss() override; + protected: void keyPressEvent(QKeyEvent* event) override; @@ -47,7 +56,7 @@ signals: private slots: void onModuleButtonClicked(int index); // Enters remove mode, or leaves it when it is already active (REQ-MOD-REMOVE). - // Reached by the Remove button and by the Q key, which leaves the mode on its way + // Reached by the Remove button and by a dismissal, which leaves the mode on its way // out of the dialog (REQ-UI-DIALOG-DISMISS). void onRemoveButtonClicked(); void onConfirm();