From 44db26c45985603f3b57009caee77368bc7f017a Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 12:07:13 +0300 Subject: [PATCH 1/2] Measure a wrapped label against a width it actually has The publisher note is written while the settings window is still being built, when its label is eight pixels wide. Wrapped against that width the sentence came out a hundred and twenty lines tall, and the minimum taken from it did not stay a minimum: QLabel folds the widget's minimum size into its own cached size hints and clears that cache only when the text changes. So the row stood two thousand pixels tall, carrying the model box, the status line and the options under it off the bottom of the window, and picking another publisher was what brought them back. Nothing to measure against yet means nothing to claim yet. The show and the resize come back for it once there is a real width. --- dikte/settings_ui.py | 26 +++++++++++++++++++++----- tests/test_ui.py | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 3f32a72..44c99e1 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -181,18 +181,34 @@ class WrappedLabel(QLabel): super().setText(text) self._fit() + def showEvent(self, event): + # Text set while the window was still being built was measured against + # nothing; this is the first moment the width means anything. + super().showEvent(event) + self._fit() + def resizeEvent(self, event): super().resizeEvent(event) self._fit() def _fit(self): + # A label the layout has not placed yet is a handful of pixels wide, + # and wrapping a sentence against that width invents a hundred lines. + # The minimum set from it does not stay a minimum either: QLabel folds + # it into its own cached size hints and clears that cache only when the + # text changes, so the row stands thousands of pixels tall and carries + # the model box and everything under it off the bottom of the window + # until another publisher is picked. Nothing to measure against yet + # means nothing to claim yet, and the show and resize above come back + # for it. + if not self.isVisible() or self.width() <= 0: + return # Measured off the font rather than asked of the label, whose own answer # is floored by the minimum set here a moment ago and so only ever grows. - if self.width() > 0: - wrap = Qt.TextFlag.TextWordWrap | Qt.TextFlag.TextWrapAnywhere - box = QRect(0, 0, self.width(), 0) - self.setMinimumHeight( - self.fontMetrics().boundingRect(box, wrap, self.text()).height()) + wrap = Qt.TextFlag.TextWordWrap | Qt.TextFlag.TextWrapAnywhere + box = QRect(0, 0, self.width(), 0) + self.setMinimumHeight( + self.fontMetrics().boundingRect(box, wrap, self.text()).height()) class WheelGuard(QObject): diff --git a/tests/test_ui.py b/tests/test_ui.py index 1324f33..cc239fb 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -244,6 +244,32 @@ class Settings(DikteTest): label.resize(2000, line) self.assertLessEqual(label.minimumHeight(), line) + def test_a_label_written_before_the_layout_places_it_claims_nothing(self): + # The publisher note is written while the settings window is still + # being built, when the label is a handful of pixels wide. Wrapped + # against that width the sentence became a hundred lines, and the + # minimum taken from it did not stay a minimum: QLabel folds it into + # its own cached size hints and clears that cache only when the text + # changes. The group box stood thousands of pixels tall, with the + # model box and everything under it off the bottom of the window, + # until another publisher was picked. + label = settings_ui.WrappedLabel() + self.addCleanup(label.deleteLater) + line = label.fontMetrics().height() + label.resize(8, line) + label.setText("Google Gemma 4, the small one. The default: nothing " + "else this size follows an instruction as closely, and " + "cleanup is all instruction.") + self.assertEqual(label.minimumHeight(), 0) + # Placed and shown, which is the first width worth measuring against. + # The room the wrapping needs is claimed then, and it is the lines the + # sentence actually takes at this width rather than at the last one. + label.resize(400, line) + label.show() + self.assertGreater(label.minimumHeight(), line) + self.assertLessEqual(label.minimumHeight(), 4 * line) + self.assertLessEqual(label.sizeHint().height(), 4 * line) + def test_saving_without_touching_anything_changes_nothing(self): """Every widget has to load what is stored, or Save writes its default over it. This says so for the whole table at once.""" From 22d2a40341c8cc553100cfe8f513fbe8c68031f1 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Sat, 5 Sep 2026 12:10:58 +0300 Subject: [PATCH 2/2] Count the lines off the font, not off this machine's font The new test pinned the wrapped height at four lines, which is four lines on a Linux runner and four and a half on a Windows one, where the same sentence in the same 400 pixels needs 54 of the box's 48. What the test is actually about is that the height comes from the width the label has now rather than the eight pixels it had while the window was being built, so it measures that width itself and compares against the answer. --- tests/test_ui.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/test_ui.py b/tests/test_ui.py index cc239fb..f759bf7 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -14,7 +14,7 @@ import unittest from typing import ClassVar from unittest import mock -from PyQt6.QtCore import QPoint, QPointF, Qt +from PyQt6.QtCore import QPoint, QPointF, QRect, Qt from PyQt6.QtGui import QWheelEvent from PyQt6.QtWidgets import QApplication, QMessageBox @@ -263,12 +263,19 @@ class Settings(DikteTest): self.assertEqual(label.minimumHeight(), 0) # Placed and shown, which is the first width worth measuring against. # The room the wrapping needs is claimed then, and it is the lines the - # sentence actually takes at this width rather than at the last one. + # sentence takes at this width rather than at the last one. Counted + # off the font rather than written down here, because how many lines + # 400 pixels hold is a different answer on every machine. label.resize(400, line) label.show() - self.assertGreater(label.minimumHeight(), line) - self.assertLessEqual(label.minimumHeight(), 4 * line) - self.assertLessEqual(label.sizeHint().height(), 4 * line) + wrap = Qt.TextFlag.TextWordWrap | Qt.TextFlag.TextWrapAnywhere + needed = label.fontMetrics().boundingRect( + QRect(0, 0, 400, 0), wrap, label.text()).height() + self.assertGreater(needed, line) # or the sentence never wrapped + self.assertEqual(label.minimumHeight(), needed) + # And the label's own hints are the wrapping at this width too, not + # the hundred lines the eight pixel one asked for. + self.assertLessEqual(label.sizeHint().height(), 3 * needed) def test_saving_without_touching_anything_changes_nothing(self): """Every widget has to load what is stored, or Save writes its default