From fd693d0b082fecbc948c1fac900f0f0e95320477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nilsson=20Str=C3=B6m?= Date: Tue, 31 Mar 2026 23:49:32 +0200 Subject: [PATCH] Allow commit info dialog to be resized Replace commit info window with a custom dialog box to allow it to be resized. Add CommitInfoDialog as a QDialog with the same info and behaviour. The main difference is that the the dialog is user-resizable with a size grip if the details box is shown. Clicking "Show Details..." or "Hide Details..." will enable or disable resizing. Adjust tests and translation catalog for the new dialog. Note: Language files need to be regenerated --- gitfourchette/forms/commitinfodialog.py | 121 ++++++++++++++++++++++++ gitfourchette/tasks/misctasks.py | 30 ++---- test/test_gpgsigning.py | 4 +- test/test_graphview.py | 15 +-- test/test_tasks_blame.py | 2 +- test/util.py | 35 +++++++ 6 files changed, 174 insertions(+), 33 deletions(-) create mode 100644 gitfourchette/forms/commitinfodialog.py 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): {table}
""" - 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 = ""