-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ENH: dynamic font size patching utility
- Loading branch information
Showing
5 changed files
with
283 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
570 dynamic_fontsize | ||
#################### | ||
|
||
API Changes | ||
----------- | ||
- N/A | ||
|
||
Features | ||
-------- | ||
- Added dynamic font sizer utility which can work with some Qt-provided widgets | ||
as well as PyDM widgets. | ||
|
||
Bugfixes | ||
-------- | ||
- N/A | ||
|
||
Maintenance | ||
----------- | ||
- N/A | ||
|
||
Contributors | ||
------------ | ||
- klauer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
""" | ||
Dynamic font size helper utilities: | ||
Dynamically set widget font size based on its current size. | ||
""" | ||
|
||
from qtpy import QtCore, QtGui, QtWidgets | ||
from qtpy.QtCore import QRectF, Qt | ||
|
||
|
||
def get_widget_maximum_font_size( | ||
widget: QtWidgets.QWidget, | ||
text: str, | ||
*, | ||
pad_width: float = 0.0, | ||
pad_height: float = 0.0, | ||
precision: float = 0.5, | ||
) -> float: | ||
""" | ||
Get the maximum font size for the given widget. | ||
Parameters | ||
---------- | ||
widget : QtWidgets.QWidget | ||
The widget to check. | ||
text : str | ||
The text for the widget to contain. | ||
pad_width : float, optional | ||
Padding to be used to reduce the size of the contents rectangle. | ||
pad_height : float, optional | ||
Padding to be used to reduce the size of the contents rectangle. | ||
precision : float | ||
Font size precision. | ||
Returns | ||
------- | ||
float | ||
""" | ||
font = widget.font() | ||
widget_contents_rect = QRectF(widget.contentsRect()) | ||
target_width = widget_contents_rect.width() - pad_width | ||
target_height = widget_contents_rect.height() - pad_height | ||
|
||
# QRectF new_rect | ||
current_size = font.pointSizeF() | ||
|
||
if not text or target_width <= 0 or target_height <= 0: | ||
return current_size | ||
|
||
step = current_size / 2.0 | ||
|
||
# If too small, increase step | ||
if step <= precision: | ||
step = precision * 4.0 | ||
|
||
last_tested_size = current_size | ||
curent_height = 0.0 | ||
current_width = 0.0 | ||
|
||
# Only stop when step is small enough and new size is smaller than QWidget | ||
while ( | ||
step > precision | ||
or (curent_height > target_height) | ||
or (current_width > target_width) | ||
): | ||
# Keep last tested value | ||
last_tested_size = current_size | ||
|
||
# Test label with its font | ||
font.setPointSizeF(current_size) | ||
# Use font metrics to test | ||
fm = QtGui.QFontMetricsF(font) | ||
|
||
# Check if widget is QLabel | ||
if isinstance(widget, QtWidgets.QLabel): | ||
if widget.wordWrap(): | ||
flags = Qt.TextFlag.TextWordWrap | widget.alignment() | ||
else: | ||
flags = widget.alignment() | ||
new_rect = fm.boundingRect(widget_contents_rect, flags, text) | ||
else: | ||
new_rect = fm.boundingRect(widget_contents_rect, 0, text) | ||
|
||
curent_height = new_rect.height() | ||
current_width = new_rect.width() | ||
|
||
# If new font size is too big, decrease it | ||
if (curent_height > target_height) or (current_width > target_width): | ||
current_size -= step | ||
# if step is small enough, keep it constant, so it converges to | ||
# biggest font size | ||
if step > precision: | ||
step /= 2.0 | ||
# Do not allow negative size | ||
if current_size <= 0: | ||
break | ||
else: | ||
# If new font size is smaller than maximum possible size, increase | ||
# it | ||
current_size += step | ||
|
||
return last_tested_size | ||
|
||
|
||
def patch_widget( | ||
widget: QtWidgets.QWidget, | ||
*, | ||
pad_percent: float = 0.0, | ||
) -> None: | ||
""" | ||
Patch the widget to dynamically change its font. | ||
Parameters | ||
---------- | ||
widget : QtWidgets.QWidget | ||
The widget to patch. | ||
pad_percent : float, optional | ||
The normalized padding percentage (0.0 - 1.0) to use in determining the | ||
maximum font size. Content margin settings determine the content | ||
rectangle, and this padding is applied as a percentage on top of that. | ||
""" | ||
def paintEvent(event: QtGui.QPaintEvent) -> None: | ||
font = widget.font() | ||
font_size = get_widget_maximum_font_size( | ||
widget, widget.text(), | ||
pad_width=widget.width() * pad_percent, | ||
pad_height=widget.height() * pad_percent, | ||
) | ||
if abs(font.pointSizeF() - font_size) > 0.1: | ||
font.setPointSizeF(font_size) | ||
widget.setFont(font) | ||
return orig_paint_event(event) | ||
|
||
def minimumSizeHint() -> QtCore.QSize: | ||
# Do not give any size hint as it it changes during paintEvent | ||
return QtWidgets.QWidget.minimumSizeHint(widget) | ||
|
||
def sizeHint() -> QtCore.QSize: | ||
# Do not give any size hint as it it changes during paintEvent | ||
return QtWidgets.QWidget.sizeHint(widget) | ||
|
||
if hasattr(widget.paintEvent, "_patched_methods_"): | ||
return | ||
|
||
orig_paint_event = widget.paintEvent | ||
|
||
paintEvent._patched_methods_ = ( | ||
widget.paintEvent, | ||
widget.sizeHint, | ||
widget.minimumSizeHint, | ||
) | ||
widget.paintEvent = paintEvent | ||
widget.sizeHint = sizeHint | ||
widget.minimumSizeHint = minimumSizeHint | ||
|
||
|
||
def unpatch_widget(widget: QtWidgets.QWidget) -> None: | ||
""" | ||
Remove dynamic font size patch from the widget, if previously applied. | ||
Parameters | ||
---------- | ||
widget : QtWidgets.QWidget | ||
The widget to unpatch. | ||
""" | ||
if not hasattr(widget.paintEvent, "_patched_methods_"): | ||
return | ||
|
||
( | ||
widget.paintEvent, | ||
widget.sizeHint, | ||
widget.minimumSizeHint, | ||
) = widget.paintEvent._patched_methods_ | ||
|
||
|
||
def is_patched(widget: QtWidgets.QWidget) -> bool: | ||
""" | ||
Check if widget has been patched for dynamically-resizing fonts. | ||
Parameters | ||
---------- | ||
widget : QtWidgets.QWidget | ||
The widget to check. | ||
Returns | ||
------- | ||
bool | ||
True if the widget has been patched. | ||
""" | ||
return hasattr(widget.paintEvent, "_patched_methods_") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
from __future__ import annotations | ||
|
||
import pytest | ||
import pytestqt.qtbot | ||
from qtpy import QtCore, QtGui, QtWidgets | ||
|
||
from typhos.tests import conftest | ||
|
||
from ..dynamic_font import is_patched, patch_widget, unpatch_widget | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"cls", | ||
[ | ||
QtWidgets.QLabel, | ||
QtWidgets.QPushButton, | ||
] | ||
) | ||
def test_patching( | ||
request: pytest.FixtureRequest, | ||
cls: type[QtWidgets.QWidget], | ||
qtbot: pytestqt.qtbot.QtBot, | ||
) -> None: | ||
widget = cls() | ||
widget.setText("Test\ntext") | ||
widget.setFixedSize(500, 500) | ||
qtbot.add_widget(widget) | ||
|
||
original_font_size = widget.font().pointSizeF() | ||
conftest.save_image( | ||
widget, | ||
f"{request.node.name}_{cls.__name__}_default_font_size", | ||
) | ||
print("Starting font size", original_font_size) | ||
|
||
event = QtGui.QPaintEvent(QtCore.QRect(0, 0, widget.width(), widget.height())) | ||
|
||
assert not is_patched(widget) | ||
patch_widget(widget) | ||
assert is_patched(widget) | ||
|
||
widget.paintEvent(event) | ||
new_font_size = widget.font().pointSizeF() | ||
print("Patched font size", new_font_size) | ||
assert original_font_size != new_font_size | ||
|
||
assert is_patched(widget) | ||
unpatch_widget(widget) | ||
assert not is_patched(widget) | ||
|
||
conftest.save_image( | ||
widget, | ||
f"{request.node.name}_{cls.__name__}_dynamic_font_size", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters