diff --git a/gitfourchette/forms/commitinfodialog.py b/gitfourchette/forms/commitinfodialog.py
new file mode 100644
index 00000000..12962ec4
--- /dev/null
+++ b/gitfourchette/forms/commitinfodialog.py
@@ -0,0 +1,121 @@
+# -----------------------------------------------------------------------------
+# Copyright (C) 2026 Iliyas Jorio.
+# This file is part of GitFourchette, distributed under the GNU GPL v3.
+# For full terms, see the included LICENSE file.
+# -----------------------------------------------------------------------------
+
+from gitfourchette.localization import _
+from gitfourchette.qt import *
+from gitfourchette.toolbox.iconbank import stockIcon
+
+
+class CommitInfoDialog(QDialog):
+ """
+ Rich summary (HTML) plus optional read-only body, in a resizable dialog.
+ Used instead of QMessageBox.setDetailedText because QMessageBox forces a
+ fixed size whenever its layout updates.
+ """
+
+ def __init__(
+ self,
+ parent: QWidget | None,
+ title: str,
+ summaryHtml: str,
+ body: str = "",
+ ):
+ super().__init__(parent)
+
+ self.setWindowTitle(title)
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
+ self.setWindowModality(Qt.WindowModality.WindowModal)
+
+ pm = stockIcon("SP_MessageBoxInformation").pixmap(48, 48)
+ iconLabel = QLabel(self)
+ iconLabel.setPixmap(pm)
+ iconLabel.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
+
+ self.summaryLabel = QLabel(self)
+ self.summaryLabel.setObjectName("commit_info_summary")
+ self.summaryLabel.setTextFormat(Qt.TextFormat.RichText)
+ self.summaryLabel.setText(summaryHtml)
+ self.summaryLabel.setWordWrap(True)
+ self.summaryLabel.setOpenExternalLinks(False)
+ self.summaryLabel.setTextInteractionFlags(
+ Qt.TextInteractionFlag.TextBrowserInteraction)
+ self.summaryLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
+
+ self._detailsEdit: QPlainTextEdit | None = None
+ self._detailsToggleButton: QAbstractButton | None = None
+
+ textColumn = QVBoxLayout()
+ textColumn.setSpacing(12)
+ textColumn.addWidget(self.summaryLabel)
+ if body:
+ self._detailsEdit = QPlainTextEdit(self)
+ self._detailsEdit.setPlainText(body)
+ self._detailsEdit.setReadOnly(True)
+ self._detailsEdit.setMinimumHeight(200)
+ self._detailsEdit.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
+ textColumn.addWidget(self._detailsEdit, stretch=1)
+
+ row = QHBoxLayout()
+ row.addWidget(iconLabel, alignment=Qt.AlignmentFlag.AlignTop)
+ row.addLayout(textColumn, stretch=1)
+
+ buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok, parent=self)
+ buttonBox.accepted.connect(self.accept)
+ buttonBox.rejected.connect(self.reject)
+ ok = buttonBox.button(QDialogButtonBox.StandardButton.Ok)
+ if ok:
+ ok.setDefault(True)
+
+ if body:
+ self._detailsToggleButton = buttonBox.addButton(
+ _("Hide Details..."), QDialogButtonBox.ButtonRole.ActionRole)
+ self._detailsToggleButton.clicked.connect(self._toggleDetailsPane)
+
+ self._outerLayout = QVBoxLayout(self)
+ self._outerLayout.addLayout(row, stretch=1 if body else 0)
+ self._outerLayout.addWidget(buttonBox)
+
+ self.setMinimumWidth(480)
+ self._resizableMinimumSize = self.minimumSize()
+ self._resizableMaximumSize = self.maximumSize()
+ self._detailsVisible = bool(body)
+ QTimer.singleShot(0, self._applyResizePolicy)
+
+ def _toggleDetailsPane(self):
+ if not self._detailsEdit or not self._detailsToggleButton:
+ return
+ show = not self._detailsEdit.isVisible()
+ self._detailsEdit.setVisible(show)
+ self._detailsVisible = show
+ self._detailsToggleButton.setText(
+ _("Hide Details...") if show else _("Show Details..."))
+
+ # No extra vertical slack above the button row when details are hidden.
+ self._outerLayout.setStretch(0, 1 if show else 0)
+ self._outerLayout.activate()
+ self.updateGeometry()
+
+ self._applyResizePolicy()
+
+ def _applyResizePolicy(self):
+ minHint = self.minimumSizeHint()
+ if self._detailsVisible:
+ minSize = QSize(self._resizableMinimumSize)
+ minSize.setWidth(max(480, minSize.width()))
+ minSize.setHeight(max(minHint.height(), minSize.height()))
+ self.setSizeGripEnabled(True)
+ self.setMinimumSize(minSize)
+ self.setMaximumSize(self._resizableMaximumSize)
+ if self.height() < minSize.height():
+ self.resize(max(self.width(), minSize.width()), minSize.height())
+ return
+
+ compactSize = QSize(minHint)
+ compactSize.setWidth(max(480, compactSize.width()))
+ self.setSizeGripEnabled(False)
+ self.resize(compactSize)
+ self.setMinimumSize(compactSize)
+ self.setMaximumSize(compactSize)
diff --git a/gitfourchette/tasks/misctasks.py b/gitfourchette/tasks/misctasks.py
index 4e9d883d..83ffdf77 100644
--- a/gitfourchette/tasks/misctasks.py
+++ b/gitfourchette/tasks/misctasks.py
@@ -11,6 +11,7 @@
from pathlib import Path
from gitfourchette import settings
+from gitfourchette.forms.commitinfodialog import CommitInfoDialog
from gitfourchette.forms.ignorepatterndialog import IgnorePatternDialog
from gitfourchette.forms.reposettingsdialog import RepoSettingsDialog
from gitfourchette.gitdriver.parsers import iterateLines
@@ -169,29 +170,12 @@ def tableRow(th, td):
"""
- messageBox = asyncMessageBox(
- self.parentWidget(), "information", title, markup,
- buttons=QMessageBox.StandardButton.Ok, macShowTitle=False)
-
- if details:
- messageBox.setDetailedText(details)
-
- # Pre-click "Show Details" button
- for button in messageBox.buttons():
- role = messageBox.buttonRole(button)
- if role == QMessageBox.ButtonRole.ActionRole:
- button.click()
- elif role == QMessageBox.ButtonRole.AcceptRole:
- messageBox.setDefaultButton(button)
-
- # Bind links to callbacks
- label: QLabel = messageBox.findChild(QLabel, "qt_msgbox_label")
- assert label
- label.setOpenExternalLinks(False)
- label.linkActivated.connect(linkBundle.processLink)
- label.linkActivated.connect(messageBox.accept)
-
- yield from self.flowDialog(messageBox)
+ dialog = CommitInfoDialog(self.parentWidget(), title, markup, details)
+
+ dialog.summaryLabel.linkActivated.connect(linkBundle.processLink)
+ dialog.summaryLabel.linkActivated.connect(dialog.accept)
+
+ yield from self.flowDialog(dialog)
class VerifyGpgSignature(RepoTask):
diff --git a/test/test_gpgsigning.py b/test/test_gpgsigning.py
index 2b25468d..eb056203 100644
--- a/test/test_gpgsigning.py
+++ b/test/test_gpgsigning.py
@@ -161,7 +161,7 @@ def testCommitWithPgpSignature(tempDir, mainWindow, tempGpgHome, amend):
# Look for GPG signing information in GetCommitInfo dialog
triggerMenuAction(mainWindow.menuBar(), "view/go to head")
triggerContextMenuAction(rw.graphView.viewport(), "get info")
- findQMessageBox(rw, f"signature:.+good signature; key trusted.+{aliceKeyId}").reject()
+ rejectCommitInfoDialog(rw, f"signature:.+good signature; key trusted.+{aliceKeyId}")
runGit("verify-commit", "-v", str(commit.id), directory=wd)
@@ -217,7 +217,7 @@ def testCommitWithSshSignature(tempDir, mainWindow, tempGpgHome, amend, passphra
# Look for signing information in GetCommitInfo dialog
triggerMenuAction(mainWindow.menuBar(), "view/go to head")
triggerContextMenuAction(rw.graphView.viewport(), "get info")
- findQMessageBox(rw, "signature:.+good signature; key trusted").reject()
+ rejectCommitInfoDialog(rw, "signature:.+good signature; key trusted")
@requiresGpg
diff --git a/test/test_graphview.py b/test/test_graphview.py
index 9de1ecd2..3b882b71 100644
--- a/test/test_graphview.py
+++ b/test/test_graphview.py
@@ -388,10 +388,11 @@ def testCommitInfo(tempDir, mainWindow, method):
else:
raise NotImplementedError(f"unknown method {method}")
- qmb = findQMessageBox(rw, "Merge branch 'a' into c")
- assert str(oid1) in qmb.text()
- assert "A U Thor" in qmb.text()
- qmb.accept()
+ dlg = findCommitInfoDialog(rw, "Merge branch 'a' into c")
+ summary = dlg.summaryLabel.text()
+ assert str(oid1) in summary
+ assert "A U Thor" in summary
+ dlg.accept()
def testCommitInfoJumpToParent(tempDir, mainWindow):
@@ -401,10 +402,10 @@ def testCommitInfoJumpToParent(tempDir, mainWindow):
rw.jump(NavLocator.inCommit(oid1, "a/a1.txt"), check=True)
triggerContextMenuAction(rw.graphView.viewport(), "get info")
- qmb = findQMessageBox(rw, "Merge branch 'a' into c")
+ dlg = findCommitInfoDialog(rw, "Merge branch 'a' into c")
- # Click on a "parent" link; this should close the message box and jump to another commit
- label = qmb.findChild(QLabel, "qt_msgbox_label")
+ # Click on a "parent" link; this should close the dialog and jump to another commit
+ label = dlg.summaryLabel
parentLink = re.search(r'6462e7d.+', label.text(), re.I).group(1)
label.linkActivated.emit(parentLink)
assert rw.navLocator.commit == Oid(hex="6462e7d8024396b14d7651e2ec11e2bbf07a05c4")
diff --git a/test/test_tasks_blame.py b/test/test_tasks_blame.py
index 5e5ca246..e8acc1a4 100644
--- a/test/test_tasks_blame.py
+++ b/test/test_tasks_blame.py
@@ -297,7 +297,7 @@ def testBlameContextMenu(blameWindow):
assert NavLocator.inCommit(BlameFixture.revs["initial"], "hello.txt").isSimilarEnoughTo(rw.navLocator)
triggerContextMenuAction(viewport, "commit info")
- acceptQMessageBox(blameWindow, r"first commit.+acecd5e.+j\. david")
+ acceptCommitInfoDialog(blameWindow, r"first commit.+acecd5e.+j\. david")
qcbSetIndex(scrubber, "uncommitted")
triggerContextMenuAction(viewport, "show diff in working directory")
diff --git a/test/util.py b/test/util.py
index 2078c058..cb49831a 100644
--- a/test/util.py
+++ b/test/util.py
@@ -640,6 +640,41 @@ def attempt() -> RepoWidget | None:
return rw
+def findCommitInfoDialog(parent: QWidget, contentPattern: str):
+ """Find a visible :class:`~gitfourchette.forms.commitinfodialog.CommitInfoDialog` by text in summary or body."""
+ from gitfourchette.forms.commitinfodialog import CommitInfoDialog
+
+ numFound = 0
+ haystack = ""
+ for dlg in parent.findChildren(CommitInfoDialog):
+ if not dlg.isVisibleTo(parent):
+ continue
+ numFound += 1
+ parts = [dlg.windowTitle()]
+ for w in dlg.findChildren(QLabel):
+ parts.append(w.text())
+ for w in dlg.findChildren(QPlainTextEdit):
+ parts.append(w.toPlainText())
+ haystack = stripHtml("\n".join(parts))
+ if re.search(contentPattern, haystack, re.IGNORECASE | re.DOTALL):
+ return dlg
+
+ raise KeyError(
+ f'did not find "{contentPattern}" among {numFound} CommitInfoDialogs. Last haystack: {haystack!r}')
+
+
+def acceptCommitInfoDialog(parent: QWidget, contentPattern: str):
+ findCommitInfoDialog(parent, contentPattern).accept()
+ parent.activateWindow()
+ QTest.qWait(0)
+
+
+def rejectCommitInfoDialog(parent: QWidget, contentPattern: str):
+ findCommitInfoDialog(parent, contentPattern).reject()
+ parent.activateWindow()
+ QTest.qWait(0)
+
+
def findQMessageBox(parent: QWidget, textPattern: str) -> QMessageBox:
numBoxesFound = 0
haystack = ""