qt/qml: labels to require opt-in to rich text - #10972
SomberNight wants to merge 11 commits into
Conversation
b180bd5 to
0c88fea
Compare
1e37407 to
d94ef5c
Compare
This will allow creating custom components with the same name as
the built-in components, and shadowing the built-in components.
i.e. we can have a file `electrum/gui/qml/components/controls/Label.qml`:
```
import QtQuick.Controls as Controls
Controls.Label {
textFormat: Text.PlainText
}
```
When referring to `Label` from another qml file that imports
`electrum/gui/qml/components/controls`, our custom Label.qml
will be used instead of the built-in `QtQuick.Controls.Label`.
You have to opt-in to rich text if you want it for your label!
Upstream chose convenience-by-default for their GUI framework. Shame. :)
-----
earlier, less satisfactory attempt:
```
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if not isinstance(event, QtCore.QChildEvent):
return False
# - ChildAdded event: already constructed QWidget gets parented
# - note: does *not* fire if parent gets set at construction time, e.g. Label("", parent=self)
# - ChildPolished event:
# - see https://doc.qt.io/qt-6/qstyle.html#polish :
# > This function is called for every widget at some point after it has been fully created
# > but just before it is shown for the very first time.
if event.type() not in (QEvent.Type.ChildAdded, QEvent.Type.ChildPolished):
return False
child = event.child()
if not isinstance(child, QLabel):
return False
if child.textFormat() != Qt.TextFormat.AutoText:
# non-default textFormat => we leave it alone
return False
child.setTextFormat(Qt.TextFormat.PlainText)
return False
```
- InjectNoRichTextEventFilter is changing AutoText to PlainText, and that also affects the internal QLabel inside the message box
d94ef5c to
28db4ce
Compare
|
The Timelock recovery plugin now renders HTML as text. And here's the after: Removing the markup from - "description": "<br/>This plug-in allows you to create Timelock Recovery Plans for your wallet. See: <a href='https://timelockrecovery.com'>timelockrecovery.com</a>",
+ "description": "This plug-in allows you to create Timelock Recovery Plans for your wallet. See: https://timelockrecovery.com", |
Thanks. Fixed in f26429e |
|
I was thinking of a workaround the long tooltips and I think a utility function that wraps the the text before setting the tooltip could do? something like: import textwrap
def wrap_tooltip_text(text: str, *, width: int = 60) -> str:
return "\n".join(
textwrap.fill(line, width=width, break_on_hyphens=False)
for line in text.split("\n")
)and then here: cb.setToolTip(wrap_tooltip_text(long_desc))and the same in Here's a before: and after: |
tooltips use QLabel internally, so due to InjectNoRichTextEventFilter, their textFormat is no longer AutoText, but is now PlainText instead. note: messages.to_rtf() was specifically introduced for usage with tooltips [0], I guess to nudge Qt to break longer lines more often (so that tooltips widths are smaller). Tooltips being forced to PlainText breaks that. I also cannot see an easy way to opt-in the tooltip of e.g. a QCheckBox to RichText. Instead, now we use the "textwrap" module from the stdlib to manually word-wrap the strings. [0]: spesmilo@345c2b4 Co-authored-by: Alwoch <salwoch@gmail.com>
f26429e to
3067352
Compare
Oh cool! Thanks. I did not know about that module in the standard library :) |
- rich text can contain embedded base64-encoded images - qt docs say [0] it is not safe to parse untrusted input as an image [0]: https://doc.qt.io/qt-6/qtimageformats-index.html : > Security Considerations: > Since these file formats are more rarely used, the codecs may be > less thoroughly debugged against potential security holes. As always, > care should be taken when creating applications that may be used > to decode uncontrolled data files.
| class InjectNoRichTextEventFilter(QObject): | ||
| """Set the default textFormat of all QLabels to PlainText. | ||
|
|
||
| note: this also affects e.g. QMessageBox as it uses a QLabel internally. | ||
| """ | ||
| def eventFilter(self, obj: QObject, event: QEvent) -> bool: | ||
| if event.type() != QEvent.Type.Polish: | ||
| # see https://doc.qt.io/qt-6/qstyle.html#polish : | ||
| # > This function [QStyle.polish()] is called for every widget at some point after | ||
| # > it has been fully created but just before it is shown for the very first time. | ||
| return False |
There was a problem hiding this comment.
question: if obj is a QLabel and it contains rich text, has the rich text already been parsed and acted upon by the time we get here and the Polish event is emitted? For example, if the rich text contains and embedded base64-encoded PNG and there is a vuln in the PNG parser, is it already too late?
3067352 to
1d5de54
Compare




I want ~all text in the GUIs to be plaintext by default. Every text field that expects/wants to render rich text must explicitly opt-in to that behaviour.
QLabel in QtWidgets, and Label in QML, default their textFormat to AutoText, which means they allow and render rich text as needed.
It is not just labels though: QML also has at least TextArea and TextEdit, and QtWidgets has QMessageBox.
The designers of Qt chose convenience-by-default, but I want security (or rather, sanity)-by-default.
(I don't know whether this PR is the best way to implement this though, and I don't mind if we come up with something else instead, all I care about is the end-result described in the first paragraph.)