-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ENH: dynamic font resizing functionality #570
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't used and probably should be removed
I erroneously thought that
TyphosLabel
et al were in the designer, but these are really used exclusively for auto-generated device displays (where we have the ophyd signal at instantiation time, etc)