give the escape menu and the name prompts the game's own frame
The last dialogs wearing system chrome were the three QMessageBoxes -- the escape menu, the game-over and win screens -- and the two QInputDialogs that name a blueprint. They are the ones the player meets at the sharpest moments of a run, and they looked like alerts from the operating system. MessageDialog and NameInputDialog replace them on the layer. The message dialog names its buttons by the index addButton hands back and reports which was clicked, with Escape standing for a button the caller nominates rather than for a dismissal of its own -- Continue in the escape menu, as it was explicitly set to before, and Quit on the two state screens, which is where the reject role sent it. Neither dialog takes Q or a click outside: every button is a decision, and a half-typed name is work in progress (REQ-UI-DIALOG-DISMISS). Two things the layer needed for the nested case: it now tracks the scroll area each modal is shown in, so a modal opened with no anchor centers on the one it was opened from rather than on the window -- which is where the Create Blueprint prompt belongs (REQ-UI-PANEL-MODAL) -- and findFor() walks a widget's parents to the layer, so the blueprint panel buried in the layout dialog can open a modal without every widget in between carrying a pointer. The three error boxes stay system dialogs on purpose: config load, config reload, and blueprint file load all report a failure that may leave nothing to draw on (REQ-UI-MODAL-CHROME). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
@@ -5,9 +5,11 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MessageDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalLayer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalPauseScope.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/NameInputDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
||||
@@ -44,8 +46,10 @@ SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MessageDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ModalLayer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/NameInputDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.cpp
|
||||
|
||||
@@ -10,10 +10,7 @@
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QInputDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QResizeEvent>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
@@ -35,8 +32,10 @@
|
||||
#include "ShipLayoutDialog.h"
|
||||
#include "BuildingIconCache.h"
|
||||
#include "ItemIconCache.h"
|
||||
#include "MessageDialog.h"
|
||||
#include "ModalLayer.h"
|
||||
#include "ModalPauseScope.h"
|
||||
#include "NameInputDialog.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
#include "VisualsLoader.h"
|
||||
@@ -257,17 +256,17 @@ void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*e
|
||||
{
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Paused"));
|
||||
QPushButton* continueBtn = box.addButton(tr("Continue"), QMessageBox::AcceptRole);
|
||||
QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::ResetRole);
|
||||
QPushButton* quitBtn = box.addButton(tr("Quit"), QMessageBox::DestructiveRole);
|
||||
box.setEscapeButton(continueBtn);
|
||||
box.exec();
|
||||
MessageDialog box(tr("Paused"), QString(), m_modalLayer);
|
||||
const int continueIndex = box.addButton(tr("Continue"));
|
||||
const int restartIndex = box.addButton(tr("Restart"));
|
||||
const int quitIndex = box.addButton(tr("Quit"));
|
||||
// Escape stands for Continue, as it did when this was a system box
|
||||
// (REQ-UI-GAME-MENU).
|
||||
box.setEscapeButtonIndex(continueIndex);
|
||||
m_modalLayer->execute(box);
|
||||
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
if (clicked == restartBtn)
|
||||
const std::optional<int> clicked = box.getClickedButtonIndex();
|
||||
if (clicked == restartIndex)
|
||||
{
|
||||
std::optional<GameConfig> newConfig = reloadConfig();
|
||||
if (!newConfig.has_value())
|
||||
@@ -285,7 +284,7 @@ void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*e
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<CommandRequestedEvent>(command));
|
||||
}
|
||||
else if (clicked == quitBtn)
|
||||
else if (clicked == quitIndex)
|
||||
{
|
||||
pause.release();
|
||||
close();
|
||||
@@ -476,14 +475,13 @@ void MainWindow::handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent>
|
||||
ModalPauseScope pause(*m_gameWorldView);
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal,
|
||||
QString(), &ok);
|
||||
NameInputDialog nameDialog(tr("Create Blueprint"), tr("Blueprint name:"),
|
||||
m_modalLayer);
|
||||
const int result = m_modalLayer->execute(nameDialog);
|
||||
// Cancel, Escape, or an empty name: no blueprint, and no selection dialog.
|
||||
if (!ok || name.trimmed().isEmpty()) { return; }
|
||||
if (result != QDialog::Accepted || nameDialog.getName().isEmpty()) { return; }
|
||||
|
||||
m_blueprintLibrary->saveSelectionAs(name.trimmed());
|
||||
m_blueprintLibrary->saveSelectionAs(nameDialog.getName());
|
||||
showBlueprintSelectionDialog();
|
||||
}
|
||||
|
||||
@@ -513,17 +511,18 @@ void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||
const int minutes = totalSeconds / 60;
|
||||
const int seconds = totalSeconds % 60;
|
||||
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Game Over"));
|
||||
box.setText(tr("HQ destroyed!\nSurvival time: %1:%2")
|
||||
.arg(minutes, 2, 10, QChar('0'))
|
||||
.arg(seconds, 2, 10, QChar('0')));
|
||||
QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::AcceptRole);
|
||||
box.addButton(tr("Quit"), QMessageBox::RejectRole);
|
||||
box.exec();
|
||||
MessageDialog box(tr("Game Over"),
|
||||
tr("HQ destroyed!\nSurvival time: %1:%2")
|
||||
.arg(minutes, 2, 10, QChar('0'))
|
||||
.arg(seconds, 2, 10, QChar('0')),
|
||||
m_modalLayer);
|
||||
const int restartIndex = box.addButton(tr("Restart"));
|
||||
const int quitIndex = box.addButton(tr("Quit"));
|
||||
// Escape quits, which is where the system box's reject role sent it.
|
||||
box.setEscapeButtonIndex(quitIndex);
|
||||
m_modalLayer->execute(box);
|
||||
|
||||
if (box.clickedButton() == restartBtn)
|
||||
if (box.getClickedButtonIndex() == restartIndex)
|
||||
{
|
||||
std::optional<GameConfig> newConfig = reloadConfig();
|
||||
if (!newConfig.has_value())
|
||||
@@ -550,17 +549,18 @@ void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/)
|
||||
const int minutes = totalSeconds / 60;
|
||||
const int seconds = totalSeconds % 60;
|
||||
|
||||
ModalLayerHold dim(*m_modalLayer);
|
||||
QMessageBox box(this);
|
||||
box.setWindowTitle(tr("Won!"));
|
||||
box.setText(tr("You collected all artifacts!\nSurvival time: %1:%2")
|
||||
.arg(minutes, 2, 10, QChar('0'))
|
||||
.arg(seconds, 2, 10, QChar('0')));
|
||||
QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::AcceptRole);
|
||||
box.addButton(tr("Quit"), QMessageBox::RejectRole);
|
||||
box.exec();
|
||||
MessageDialog box(tr("Won!"),
|
||||
tr("You collected all artifacts!\nSurvival time: %1:%2")
|
||||
.arg(minutes, 2, 10, QChar('0'))
|
||||
.arg(seconds, 2, 10, QChar('0')),
|
||||
m_modalLayer);
|
||||
const int restartIndex = box.addButton(tr("Restart"));
|
||||
const int quitIndex = box.addButton(tr("Quit"));
|
||||
// Escape quits, which is where the system box's reject role sent it.
|
||||
box.setEscapeButtonIndex(quitIndex);
|
||||
m_modalLayer->execute(box);
|
||||
|
||||
if (box.clickedButton() == restartBtn)
|
||||
if (box.getClickedButtonIndex() == restartIndex)
|
||||
{
|
||||
std::optional<GameConfig> newConfig = reloadConfig();
|
||||
if (!newConfig.has_value())
|
||||
|
||||
75
src/ui/MessageDialog.cpp
Normal file
75
src/ui/MessageDialog.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "MessageDialog.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
const int kSpacingPx = 8;
|
||||
}
|
||||
|
||||
MessageDialog::MessageDialog(const QString& title, const QString& text, QWidget* parent)
|
||||
: ModalDialog(parent)
|
||||
, m_buttonLayout(nullptr)
|
||||
, m_buttonCount(0)
|
||||
{
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx);
|
||||
mainLayout->setSpacing(kSpacingPx);
|
||||
|
||||
if (!text.isEmpty())
|
||||
{
|
||||
QLabel* textLabel = new QLabel(text, this);
|
||||
mainLayout->addWidget(textLabel);
|
||||
}
|
||||
|
||||
// The buttons sit at the right of their row, the side a dialog is confirmed from.
|
||||
m_buttonLayout = new QHBoxLayout();
|
||||
m_buttonLayout->setSpacing(kSpacingPx);
|
||||
m_buttonLayout->addStretch();
|
||||
mainLayout->addLayout(m_buttonLayout);
|
||||
|
||||
// Last, so it becomes the first row (REQ-UI-MODAL-CHROME). No close button: closing
|
||||
// is one of the buttons here, or nothing at all.
|
||||
addHeader(mainLayout, title, false);
|
||||
}
|
||||
|
||||
int MessageDialog::addButton(const QString& caption)
|
||||
{
|
||||
const int index = m_buttonCount++;
|
||||
QPushButton* button = new QPushButton(caption, this);
|
||||
m_buttonLayout->addWidget(button);
|
||||
connect(button, &QPushButton::clicked, this, [this, index]() {
|
||||
onButtonClicked(index);
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
void MessageDialog::setEscapeButtonIndex(int index)
|
||||
{
|
||||
m_escapeButtonIndex = index;
|
||||
}
|
||||
|
||||
std::optional<int> MessageDialog::getClickedButtonIndex() const
|
||||
{
|
||||
return m_clickedButtonIndex;
|
||||
}
|
||||
|
||||
void MessageDialog::reject()
|
||||
{
|
||||
// Escape stands for a button rather than for a dismissal of its own, so the caller
|
||||
// reads one answer whichever way the player gave it (REQ-UI-GAME-MENU).
|
||||
if (!m_escapeButtonIndex.has_value())
|
||||
{
|
||||
return;
|
||||
}
|
||||
onButtonClicked(*m_escapeButtonIndex);
|
||||
}
|
||||
|
||||
void MessageDialog::onButtonClicked(int index)
|
||||
{
|
||||
m_clickedButtonIndex = index;
|
||||
accept();
|
||||
}
|
||||
50
src/ui/MessageDialog.h
Normal file
50
src/ui/MessageDialog.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "ModalDialog.h"
|
||||
|
||||
class QHBoxLayout;
|
||||
class QLabel;
|
||||
|
||||
// The game's own message box (REQ-UI-MODAL-CHROME): a title, an optional line or two of
|
||||
// text, and a row of buttons the caller names. It stands in for QMessageBox at the three
|
||||
// places the player is asked to decide something -- the escape menu (REQ-UI-GAME-MENU)
|
||||
// and the game-over and win screens (REQ-HQ-GAME-OVER, REQ-WIN-SCREEN) -- so that those
|
||||
// read as part of the game rather than as system alerts.
|
||||
//
|
||||
// The caller identifies buttons by the index addButton() hands back, and asks
|
||||
// getClickedButtonIndex() afterwards; the QDialog result code says only whether a button
|
||||
// was clicked at all. Q and a click outside do not dismiss it: every button here is a
|
||||
// decision, and there is no "no change" among them to fall back on.
|
||||
class MessageDialog : public ModalDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// text may be empty, for a dialog that is a question its buttons already state.
|
||||
MessageDialog(const QString& title, const QString& text, QWidget* parent = nullptr);
|
||||
|
||||
// Appends a button and returns its index, left to right.
|
||||
int addButton(const QString& caption);
|
||||
|
||||
// Which button Escape stands for. Without one, Escape does nothing -- a dialog whose
|
||||
// buttons all commit to something has no dismissal to offer.
|
||||
void setEscapeButtonIndex(int index);
|
||||
|
||||
std::optional<int> getClickedButtonIndex() const;
|
||||
|
||||
public slots:
|
||||
void reject() override;
|
||||
|
||||
private:
|
||||
void onButtonClicked(int index);
|
||||
|
||||
QHBoxLayout* m_buttonLayout;
|
||||
int m_buttonCount;
|
||||
std::optional<int> m_escapeButtonIndex;
|
||||
std::optional<int> m_clickedButtonIndex;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "ModalLayer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QFrame>
|
||||
@@ -12,6 +12,20 @@
|
||||
|
||||
#include "ModalDialog.h"
|
||||
|
||||
ModalLayer* ModalLayer::findFor(const QWidget& widget)
|
||||
{
|
||||
for (QWidget* candidate = widget.parentWidget(); candidate != nullptr;
|
||||
candidate = candidate->parentWidget())
|
||||
{
|
||||
ModalLayer* layer = qobject_cast<ModalLayer*>(candidate);
|
||||
if (layer != nullptr)
|
||||
{
|
||||
return layer;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ModalLayer::ModalLayer(const QColor& dimColor, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_dimColor(dimColor)
|
||||
@@ -30,13 +44,31 @@ int ModalLayer::execute(ModalDialog& content, const QRect& anchorRect)
|
||||
host->setWidgetResizable(false);
|
||||
host->setWidget(&content);
|
||||
|
||||
m_stack.push_back(&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 });
|
||||
updateVisibility();
|
||||
raise();
|
||||
place(*host, content, anchorRect);
|
||||
place(*host, content, anchor);
|
||||
host->show();
|
||||
content.show();
|
||||
content.setFocus();
|
||||
|
||||
// The modal's own focus widget where it has 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).
|
||||
if (content.focusWidget() != nullptr)
|
||||
{
|
||||
content.focusWidget()->setFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
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.
|
||||
@@ -52,7 +84,14 @@ int ModalLayer::execute(ModalDialog& content, const QRect& anchorRect)
|
||||
content.hide();
|
||||
delete host;
|
||||
|
||||
m_stack.erase(std::remove(m_stack.begin(), m_stack.end(), &content), m_stack.end());
|
||||
for (std::size_t i = m_stack.size(); i > 0; --i)
|
||||
{
|
||||
if (m_stack[i - 1].content == &content)
|
||||
{
|
||||
m_stack.erase(m_stack.begin() + static_cast<std::ptrdiff_t>(i - 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateVisibility();
|
||||
return content.result();
|
||||
}
|
||||
@@ -104,9 +143,8 @@ void ModalLayer::place(QScrollArea& host, ModalDialog& content,
|
||||
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);
|
||||
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
|
||||
|
||||
@@ -24,12 +24,18 @@ 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 layer
|
||||
// itself (REQ-UI-PANEL-MODAL).
|
||||
// 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
|
||||
@@ -48,15 +54,24 @@ protected:
|
||||
void paintEvent(QPaintEvent* 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;
|
||||
};
|
||||
|
||||
// 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<ModalDialog*> m_stack; // bottom-most first; back() is the open one
|
||||
int m_holdCount = 0;
|
||||
QColor m_dimColor;
|
||||
std::vector<HostedModal> m_stack; // bottom-most first; back() is the open one
|
||||
int m_holdCount = 0;
|
||||
};
|
||||
|
||||
// RAII guard for addHold()/removeHold().
|
||||
|
||||
56
src/ui/NameInputDialog.cpp
Normal file
56
src/ui/NameInputDialog.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "NameInputDialog.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
const int kSpacingPx = 8;
|
||||
const int kMinimumWidthPx = 280;
|
||||
}
|
||||
|
||||
NameInputDialog::NameInputDialog(const QString& title, const QString& prompt,
|
||||
QWidget* parent)
|
||||
: ModalDialog(parent)
|
||||
, m_nameEdit(nullptr)
|
||||
{
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(kSpacingPx, kSpacingPx, kSpacingPx, kSpacingPx);
|
||||
mainLayout->setSpacing(kSpacingPx);
|
||||
|
||||
QLabel* promptLabel = new QLabel(prompt, this);
|
||||
mainLayout->addWidget(promptLabel);
|
||||
|
||||
m_nameEdit = new QLineEdit(this);
|
||||
m_nameEdit->setMinimumWidth(kMinimumWidthPx);
|
||||
mainLayout->addWidget(m_nameEdit);
|
||||
|
||||
QHBoxLayout* buttonLayout = new QHBoxLayout();
|
||||
buttonLayout->setSpacing(kSpacingPx);
|
||||
buttonLayout->addStretch();
|
||||
|
||||
QPushButton* confirmButton = new QPushButton(tr("Confirm"), this);
|
||||
QPushButton* cancelButton = new QPushButton(tr("Cancel"), this);
|
||||
buttonLayout->addWidget(confirmButton);
|
||||
buttonLayout->addWidget(cancelButton);
|
||||
mainLayout->addLayout(buttonLayout);
|
||||
|
||||
connect(confirmButton, &QPushButton::clicked, this, &QDialog::accept);
|
||||
connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
// Enter confirms from the line edit, as it did in the prompt this replaces.
|
||||
connect(m_nameEdit, &QLineEdit::returnPressed, this, &QDialog::accept);
|
||||
|
||||
addHeader(mainLayout, title, false);
|
||||
|
||||
// The line edit is what the player came here to use, and it takes Q as a character
|
||||
// because it holds the focus (REQ-UI-DIALOG-DISMISS).
|
||||
m_nameEdit->setFocus();
|
||||
}
|
||||
|
||||
QString NameInputDialog::getName() const
|
||||
{
|
||||
return m_nameEdit->text().trimmed();
|
||||
}
|
||||
30
src/ui/NameInputDialog.h
Normal file
30
src/ui/NameInputDialog.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "ModalDialog.h"
|
||||
|
||||
class QLineEdit;
|
||||
|
||||
// The game's own name prompt (REQ-UI-MODAL-CHROME): a title, a prompt, a line edit, and
|
||||
// Confirm and Cancel. It stands in for QInputDialog at the two places a blueprint is
|
||||
// named -- the blueprint save dialog (REQ-UI-BLUEPRINT-CREATE) and the Create Blueprint
|
||||
// dialog of the layout configuration dialog (REQ-MOD-UI-BLUEPRINT-CREATE).
|
||||
//
|
||||
// Neither Q nor a click outside dismisses it (ModalDialog::isDismissible stays false):
|
||||
// Q is a character the player may be typing, and a half-typed name is work in progress
|
||||
// that no stray gesture may discard (REQ-UI-DIALOG-DISMISS). Escape and Cancel remain.
|
||||
// The caller decides what an empty name means; the dialog reports it trimmed.
|
||||
class NameInputDialog : public ModalDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NameInputDialog(const QString& title, const QString& prompt,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
QString getName() const;
|
||||
|
||||
private:
|
||||
QLineEdit* m_nameEdit;
|
||||
};
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <functional>
|
||||
|
||||
#include "DisplayName.h"
|
||||
#include "ModalLayer.h"
|
||||
#include "NameInputDialog.h"
|
||||
#include "OptionButton.h"
|
||||
#include "ProductionRules.h"
|
||||
#include "RecipeLineRow.h"
|
||||
@@ -12,7 +14,6 @@
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
@@ -336,14 +337,18 @@ public:
|
||||
layout->addWidget(m_scrollArea, 1);
|
||||
|
||||
connect(createBtn, &QPushButton::clicked, this, [this]() {
|
||||
bool ok = false;
|
||||
const QString name = QInputDialog::getText(
|
||||
this, tr("Create Blueprint"), tr("Blueprint name:"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok || name.trimmed().isEmpty()) { return; }
|
||||
// Opened on the same layer as the dialog this panel sits in, so it stacks
|
||||
// over it on the one dim (REQ-MOD-UI-BLUEPRINT-CREATE, REQ-UI-MODAL-DIM).
|
||||
ModalLayer* layer = ModalLayer::findFor(*this);
|
||||
if (layer == nullptr) { return; }
|
||||
|
||||
NameInputDialog nameDialog(tr("Create Blueprint"), tr("Blueprint name:"));
|
||||
if (layer->execute(nameDialog) != QDialog::Accepted) { return; }
|
||||
const QString name = nameDialog.getName();
|
||||
if (name.isEmpty()) { return; }
|
||||
|
||||
ShipLayoutBlueprint bp;
|
||||
bp.name = name.trimmed();
|
||||
bp.name = name;
|
||||
bp.shipType = m_shipType;
|
||||
bp.modules = m_getModules();
|
||||
m_allBlueprints.push_back(std::move(bp));
|
||||
|
||||
Reference in New Issue
Block a user