open the panel's modals on the panel

Implements REQ-UI-PANEL-MODAL. The recipe/schematic selection dialog and
the ship layout configuration dialog are both opened from controls inside
the selection panel, but Qt centered them on the game window, sending the
cursor back across the view and then back again.

MainWindow::placeOnSelectionPanel() centers a dialog on the panel's live
global rectangle -- so a dragged panel carries the modal with it -- and
clamps it into the window without resizing it, clamping the far edge
before the near one so a dialog too large for the window lands on the
window's top-left corner. It is called on the constructed dialog before
exec(), which also stops QDialog from re-centering it on show.

Placement is all that changes: modality, the auto-pause, the modal dim and
dismissal are untouched, as are the modals not opened from the panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-11 12:22:00 +02:00
parent 11de38cf1f
commit 97531ed217
2 changed files with 50 additions and 0 deletions

View File

@@ -7,6 +7,7 @@
#include <QApplication> #include <QApplication>
#include <QCloseEvent> #include <QCloseEvent>
#include <QDialog>
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QInputDialog> #include <QInputDialog>
@@ -317,6 +318,9 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
std::move(unlockedModuleIds), std::move(unlockedModuleIds),
m_gameWorldView->isDebugDrawEnabled(), m_gameWorldView->isDebugDrawEnabled(),
this); this);
// 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 (dialog.exec() == QDialog::Accepted && dialog.getResult().has_value())
{ {
std::shared_ptr<SetShipLayoutCommand> command = std::shared_ptr<SetShipLayoutCommand> command =
@@ -328,6 +332,45 @@ void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
} }
} }
void MainWindow::placeOnSelectionPanel(QDialog& dialog) 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();
// 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);
}
void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
{ {
// A construction site has no Building yet; fall back to its site record so // A construction site has no Building yet; fall back to its site record so
@@ -385,6 +428,7 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
bool autoOpenLayout = false; bool autoOpenLayout = false;
std::string chosenSchematic; std::string chosenSchematic;
RecipeSelectionDialog dialog(options, title, m_itemIcons.get(), this); RecipeSelectionDialog dialog(options, title, m_itemIcons.get(), this);
placeOnSelectionPanel(dialog);
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value()) if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
{ {
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>(); std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();

View File

@@ -36,6 +36,7 @@ class BlueprintLibrary;
class BuildingIconCache; class BuildingIconCache;
class ItemIconCache; class ItemIconCache;
class QCloseEvent; class QCloseEvent;
class QDialog;
class QResizeEvent; class QResizeEvent;
class MainWindow : public QWidget, class MainWindow : public QWidget,
@@ -83,6 +84,11 @@ private:
const std::string& schematicId, const std::string& schematicId,
const ShipLayoutConfig& currentLayout); 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;
// Runs the blueprint selection dialog and enters placement mode for whatever the // 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: // 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 // both callers already hold theirs, which is what keeps the dim continuous when a