qt: per-target close-dialog wording, fit-to-content sizing

The single generic "There are still running processes. / Closing will
terminate the running processes." prompt was wrapping awkwardly inside
QMessageBox's default width, splitting "the running processes" across
two lines.

Thread a CloseTarget enum through confirmCloseSurfaces so each call
site (Window / Tab / Tabs / Split) gets the right question + body,
mirroring GTK's close_confirmation_dialog. The Quit / Close All
Windows prompt gets the same treatment ("Quit Ghastty?" /
"Close All Windows?" with one body line).

QMessageBox ignores setMinimumWidth because its inner QGridLayout
drives sizing from contents — a widenForText() helper measures the
body text with the box's font and adds a 0-height spacer row spanning
the grid's columns to force a min width, so the body fits on one line.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-23 16:28:01 -05:00
parent d523efd22c
commit 13f139dcf2
3 changed files with 83 additions and 19 deletions

View File

@ -14,6 +14,8 @@
#include <QEvent>
#include <QColor>
#include <QFont>
#include <QFontMetrics>
#include <QGridLayout>
#include <QGuiApplication>
#include <QIcon>
#include <QList>
@ -30,6 +32,7 @@
#include <QStandardPaths>
#include <QRect>
#include <QShowEvent>
#include <QSpacerItem>
#include <QSplitter>
#include <QStringList>
#include <QString>
@ -506,7 +509,9 @@ void MainWindow::closeTabsByMode(GhosttySurface *src,
QList<GhosttySurface *> affected;
for (int i : indices)
for (GhosttySurface *s : surfacesInTab(i)) affected.append(s);
if (!confirmCloseSurfaces(affected)) return;
const CloseTarget target =
indices.size() == 1 ? CloseTarget::Tab : CloseTarget::Tabs;
if (!confirmCloseSurfaces(affected, target)) return;
for (int i : indices) closeTab(i);
}
@ -631,7 +636,7 @@ void MainWindow::copyTitleToClipboard(GhosttySurface *src) {
}
void MainWindow::onTabCloseRequested(int index) {
if (!confirmCloseSurfaces(surfacesInTab(index))) return;
if (!confirmCloseSurfaces(surfacesInTab(index), CloseTarget::Tab)) return;
closeTab(index);
}
@ -644,7 +649,7 @@ void MainWindow::closeEvent(QCloseEvent *e) {
// resets per-action, mirroring this.
const bool skip = m_skipCloseConfirm;
m_skipCloseConfirm = false;
if (!skip && !confirmCloseSurfaces(m_surfaces)) {
if (!skip && !confirmCloseSurfaces(m_surfaces, CloseTarget::Window)) {
e->ignore();
return;
}
@ -659,8 +664,24 @@ void MainWindow::closeEvent(QCloseEvent *e) {
e->accept();
}
// Force a QMessageBox to be wide enough that `text` fits on one line.
// QMessageBox ignores setMinimumWidth because its inner QGridLayout
// drives sizing from its contents — adding a 0-height spacer in a
// row that spans the layout's columns forces that minimum.
static void widenForText(QMessageBox &box, const QString &text) {
auto *grid = qobject_cast<QGridLayout *>(box.layout());
if (!grid) return;
const QFontMetrics fm(box.font());
// Icon + grid margins + a little breathing room.
const int padding = 96;
const int minWidth = fm.horizontalAdvance(text) + padding;
grid->addItem(new QSpacerItem(minWidth, 0, QSizePolicy::Minimum,
QSizePolicy::Fixed),
grid->rowCount(), 0, 1, grid->columnCount());
}
bool MainWindow::confirmCloseSurfaces(
const QList<GhosttySurface *> &surfaces) {
const QList<GhosttySurface *> &surfaces, CloseTarget target) {
// Honor the `confirm-close-surface` config:
// false -> never prompt
// true -> prompt only when libghostty says a process is running
@ -681,18 +702,50 @@ bool MainWindow::confirmCloseSurfaces(
// Destructive-styled dialog with Cancel/Close, matching macOS
// NSAlert and GTK Adw.MessageDialog (`close-response: cancel`,
// destructive class on the close button).
// destructive class on the close button). Per-target wording
// mirrors GTK's close_confirmation_dialog so the message reads
// naturally on one line instead of splitting "the running
// processes" across two lines.
QString question;
QString body;
QString verb;
switch (target) {
case CloseTarget::Window:
question = QStringLiteral("Close Window?");
body = QStringLiteral(
"All terminal sessions in this window will be terminated.");
verb = QStringLiteral("Close");
break;
case CloseTarget::Tab:
question = QStringLiteral("Close Tab?");
body = QStringLiteral(
"All terminal sessions in this tab will be terminated.");
verb = QStringLiteral("Close");
break;
case CloseTarget::Tabs:
question = QStringLiteral("Close Tabs?");
body = QStringLiteral(
"All terminal sessions in these tabs will be terminated.");
verb = QStringLiteral("Close");
break;
case CloseTarget::Split:
question = QStringLiteral("Close Split?");
body = QStringLiteral(
"The currently running process in this split will be terminated.");
verb = QStringLiteral("Close");
break;
}
QMessageBox box(this);
box.setIcon(QMessageBox::Warning);
box.setWindowTitle(QStringLiteral("Close"));
box.setText(QStringLiteral("There are still running processes."));
box.setInformativeText(
QStringLiteral("Closing will terminate the running processes."));
QPushButton *close = box.addButton(QStringLiteral("Close"),
QMessageBox::DestructiveRole);
box.setWindowTitle(question);
box.setText(question);
box.setInformativeText(body);
QPushButton *close = box.addButton(verb, QMessageBox::DestructiveRole);
QPushButton *cancel = box.addButton(QStringLiteral("Cancel"),
QMessageBox::RejectRole);
box.setDefaultButton(cancel);
widenForText(box, body);
box.exec();
return box.clickedButton() == close;
}
@ -704,21 +757,24 @@ void MainWindow::closeAllWindows(bool thenQuit) {
// Close All Windows (process may stay alive).
ghostty_app_t app = GhosttyApp::instance().app();
if (app && ghostty_app_needs_confirm_quit(app)) {
const QString title = thenQuit ? QStringLiteral("Quit")
: QStringLiteral("Close All Windows");
const QString question = thenQuit
? QStringLiteral("Quit Ghastty?")
: QStringLiteral("Close All Windows?");
const QString body =
QStringLiteral("All terminal sessions will be terminated.");
const QString verb = thenQuit ? QStringLiteral("Quit")
: QStringLiteral("Close All");
const QList<MainWindow *> &live = GhosttyApp::instance().windows();
QMessageBox box(live.isEmpty() ? nullptr : live.first());
box.setIcon(QMessageBox::Warning);
box.setWindowTitle(title);
box.setText(QStringLiteral("There are still running processes."));
box.setInformativeText(QStringLiteral(
"%1 will terminate the running processes.").arg(title));
box.setWindowTitle(question);
box.setText(question);
box.setInformativeText(body);
QPushButton *go = box.addButton(verb, QMessageBox::DestructiveRole);
QPushButton *cancel = box.addButton(QStringLiteral("Cancel"),
QMessageBox::RejectRole);
box.setDefaultButton(cancel);
widenForText(box, body);
box.exec();
if (box.clickedButton() != go) return;
}

View File

@ -144,7 +144,14 @@ public:
void copyTitleToClipboard(GhosttySurface *src);
void toggleCommandPalette(GhosttySurface *surface);
void toggleSplitZoom(GhosttySurface *surface);
bool confirmCloseSurfaces(const QList<GhosttySurface *> &surfaces);
enum class CloseTarget {
Window, // closing the whole window
Tab, // closing a single tab
Tabs, // closing multiple tabs (Close Other / Close Right)
Split, // closing a single surface in a split
};
bool confirmCloseSurfaces(const QList<GhosttySurface *> &surfaces,
CloseTarget target);
// ---- libghostty-driven gating accessors ------------------------

View File

@ -358,7 +358,8 @@ void GhosttyApp::onCloseSurface(void *ud, bool) {
self,
[selfp, sp]() {
if (!selfp || !sp) return;
if (selfp->confirmCloseSurfaces({sp})) selfp->removeSurface(sp);
if (selfp->confirmCloseSurfaces({sp}, MainWindow::CloseTarget::Split))
selfp->removeSurface(sp);
},
Qt::QueuedConnection);
}