Skip to content

qt/qml: labels to require opt-in to rich text - #10972

Merged
ecdsa merged 12 commits into
spesmilo:masterfrom
SomberNight:202609_qt_label_plaintext_by_default
Sep 26, 2026
Merged

ecdsa merged 12 commits into
spesmilo:masterfrom
SomberNight:202609_qt_label_plaintext_by_default

Conversation

@SomberNight

@SomberNight SomberNight commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

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.)


  • For the Qt Widgets GUI, this uses an EventFilter-based approach, that listens for "Polish" QEvents.
  • For the QML GUI, only Android is covered: during the Android build, we patch Qt to change the default from AutoText to PlainText. This leaves the Linux desktop dev environment and potential Linux phone users uncovered.

Note: there is a proposal on the Qt mailing list to change the default to PlainText, in Qt7, which has some traction (?)

@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from b180bd5 to 0c88fea Compare September 14, 2026 21:55
@SomberNight
SomberNight marked this pull request as draft September 14, 2026 23:56
@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch 3 times, most recently from 1e37407 to d94ef5c Compare September 15, 2026 15:14
@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from d94ef5c to 28db4ce Compare September 15, 2026 17:51
@SomberNight
SomberNight marked this pull request as ready for review September 16, 2026 14:51
@Alwoch

Alwoch commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

The Timelock recovery plugin now renders HTML as text.
Here's a before:

pic1 Screenshot 2026-09-17 at 17 00 49

And here's the after:

pic2 Screenshot 2026-09-17 at 17 04 20

Removing the markup from plugins/timelock_recovery/manifest.json should resolve this.

-  "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",

@SomberNight

Copy link
Copy Markdown
Member Author

The Timelock recovery plugin now renders HTML as text.

Thanks. Fixed in f26429e

@Alwoch

Alwoch commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

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 QMenuWithConfig.addConfig (tooltip = wrap_tooltip_text(long_desc)).

Here's a before:

pic1 Screenshot 2026-09-17 at 18 05 59

and after:

pic2 Screenshot 2026-09-17 at 18 08 23

@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from f26429e to 3067352 Compare September 17, 2026 17:21
@SomberNight

Copy link
Copy Markdown
Member Author

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: [...]

Oh cool! Thanks. I did not know about that module in the standard library :)
Added in 51b47ae. The default params look fine.

Comment thread electrum/gui/qt/plugins_dialog.py Outdated
Comment thread electrum/gui/qt/__init__.py
@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from 3067352 to 1d5de54 Compare September 18, 2026 17:06
"name": "trustedcoin",
"fullname": "Two Factor Authentication",
"description": "This plugin adds two-factor authentication to your wallet.<br/>For more information, visit <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>",
"description": "This plugin adds two-factor authentication to your wallet.\nFor more information, visit https://api.trustedcoin.com/#/electrum-help",

@ecdsa ecdsa Sep 21, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could as well add an explicit "url" section to the manifest.json of plugins.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we can do that. Would rather not do it in this PR though.

@ecdsa

This comment was marked as outdated.

@SomberNight SomberNight added this to the 4.9.0 milestone Sep 23, 2026
Comment thread electrum/gui/qml/components/controls/AddressDelegate.qml Outdated
@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from 13c96a1 to 5ad7060 Compare September 25, 2026 14:09
@SomberNight
SomberNight marked this pull request as draft September 25, 2026 14:09
@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from 46e1f4d to 62c32c2 Compare September 25, 2026 14:46
Security-by-default, instead of convenience: require programmer to opt-in to RichText.

This patches Qt only for the Android build, and adds a runtime regression check.
We mainly use the QML GUI for Android. However this leaves the Linux desktop
dev environment and potential Linux phone users uncovered :/
@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from 62c32c2 to c2a0a03 Compare September 25, 2026 17:20
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
```
SomberNight and others added 8 commits September 25, 2026 17:54
this special-case QApplication is not covered by InjectNoRichTextEventFilter
- InjectNoRichTextEventFilter is changing AutoText to PlainText,
  and that also affects the internal QLabel inside the message box
llm output:

> Linux with a screen reader running. Here the accessibility bridge queries the label’s text from inside
> setText(), and Qt parses the HTML at that moment. This also affects custom_message_box(rich_text=False),
> which passes the text to the QMessageBox constructor before switching it to PlainText.
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>
- 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.
@SomberNight
SomberNight force-pushed the 202609_qt_label_plaintext_by_default branch from c2a0a03 to e258401 Compare September 25, 2026 17:55
@SomberNight
SomberNight marked this pull request as ready for review September 25, 2026 17:55
@SomberNight

Copy link
Copy Markdown
Member Author

Well I think this is as good as I can get it, and ready for merge.

@ecdsa
ecdsa merged commit 76dbf2b into spesmilo:master Sep 26, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants