Merge pull request #10972 from SomberNight/202609_qt_label_plaintext_by_default
What changed, and why it matters
This commit changes Electrum's user interface so that text labels default to plain text instead of rich text. Rich text (HTML-like formatting) can be abused if untrusted data is displayed, because it may allow attackers to inject clickable links, fake UI elements, or trigger bugs in the text-rendering engine. The patch makes rich text an opt-in choice for developers and explicitly marks trusted labels that need formatting. It also adds a runtime sanity check on Android and patches the Qt framework used in Android builds to make plain text the default at a lower level. This is a defensive hardening change rather than a fix for a specific known exploit.
Treat this as a security hardening patch and include it in the next release. Review any downstream forks or plugins that rely on implicit rich-text rendering in QLabel/QML labels, as they will now render as plain text unless explicitly opted in. For desktop builds, consider whether the Polish-event filter is sufficient or whether labels should be created with PlainText set before setText() is called, as noted in the code comment regarding screen readers and parser vulnerabilities.
Security signals we found
Default text format changed from AutoText/RichText to PlainText across Qt/QML widgets
New application-wide event filter forces QLabel to PlainText unless explicitly opted in
custom_message_box sets textFormat before setText() to prevent untrusted text from being parsed as rich text
Android Qt6 build patched at source level to make QQuickText default to PlainText
Runtime Android sanity check hard-fails if rich text is still the default
Plugin manifest descriptions stripped of HTML (<br/> and <a href>) to avoid rendering untrusted rich text
Tooltips converted from RTF/HTML to word-wrapped plain text
Code comment explicitly discusses risk of parser vulnerabilities and screen-reader accessibility bridge parsing HTML inside setText()
Evidence from the diff
The commit hardens Electrum’s Qt/QML GUI against rich-text injection and parser vulnerabilities by switching the default textFormat of QLabel and QML Label/Text components from AutoText/RichText to PlainText. Key changes: (1) a new InjectNoRichTextEventFilter installed on the QApplication intercepts QLabel Polish events and forces textFormat to PlainText unless the label already has a non-default format; (2) custom_message_box now sets textFormat before calling setText() to avoid parsing untrusted strings as HTML; (3) all existing labels that legitimately contain rich text are explicitly annotated with setTextFormat(Qt.TextFormat.RichText) or textFormat: Text.RichText; (4) tooltips and plugin descriptions that previously used HTML are converted to plain text via a new wrap_multi_paragraph_text helper; (5) the Android Qt6 recipe is patched to change QQuickText’s default format from AutoText to PlainText; (6) a QML runtime sanity check on Android quits the app if the default is not PlainText. The commit message and code comments frame this as a security-by-default measure.
Changed components
electrum/gui/qt/__init__.pyelectrum/gui/qt/util.pyelectrum/gui/qt/custom_message_boxelectrum/gui/qml/components/main.qmlelectrum/gui/qml/components/*contrib/android/p4a_recipes/qt6/patches/qt-6-10-rich-text-should-be-opt-in.patchelectrum/gui/messages.pyelectrum/plugins/*/manifest.jsonelectrum/plugins/trustedcoin/qt.pyelectrum/plugins/coldcard/qt.pyelectrum/plugins/revealer/qt.pyInspect captured patch +155 / −39
### contrib/android/p4a_recipes/qt6/__init__.py
@@ -13,5 +13,9 @@
class Qt6RecipePinned(util.InheritedRecipeMixin, Qt6Recipe):
sha512sum = "bf1a1d42d57b4d2e77f7227f4cbe01e847fd65035461b89481063b32f25a57be6e5a07889acc4af65ca9ff9d27b7fe63bd2fe60b8aa7fa19d554394d799fbaa1"
+ patches = Qt6Recipe.patches + [
+ os.path.join(os.path.dirname(__file__), "patches", "qt-6-10-rich-text-should-be-opt-in.patch"),
+ ]
+
recipe = Qt6RecipePinned()
### contrib/android/p4a_recipes/qt6/patches/qt-6-10-rich-text-should-be-opt-in.patch
@@ -0,0 +1,14 @@
+# Set default textFormat of Labels and all other controls to PlainText.
+# Security-by-default, instead of convenience: require programmer to opt-in to RichText.
+
+--- a/qtdeclarative/src/quick/items/qquicktext.cpp
++++ b/qtdeclarative/src/quick/items/qquicktext.cpp
+@@ -52,7 +52,7 @@ QQuickTextPrivate::QQuickTextPrivate()
+ , color(0xFF000000), linkColor(0xFF0000FF), styleColor(0xFF000000)
+ , lineCount(1), multilengthEos(-1)
+ , elideMode(QQuickText::ElideNone), hAlign(QQuickText::AlignLeft), vAlign(QQuickText::AlignTop)
+- , format(QQuickText::AutoText), wrapMode(QQuickText::NoWrap)
++ , format(QQuickText::PlainText), wrapMode(QQuickText::NoWrap)
+ , style(QQuickText::Normal)
+ , renderType(QQuickTextUtil::textRenderType<QQuickText>())
+ , updateType(UpdatePaintNode)
### electrum/gui/messages.py
@@ -1,3 +1,5 @@
+import textwrap
+
from electrum.i18n import _
from electrum.submarine_swaps import MIN_FINAL_CLTV_DELTA_FOR_CLIENT
@@ -6,6 +8,19 @@ def to_rtf(msg):
return '\n'.join(['<p>' + x + '</p>' for x in msg.split('\n\n')])
+def wrap_multi_paragraph_text(text: str) -> str:
+ """Word-wrap long lines.
+
+ - If text contains multiple paragraphs, the paragraph-separation is kept.
+ - Useful for tooltips (shown on mouse-over), as Qt otherwise
+ only word-wraps lines longer than the screen-width.
+ """
+ return "\n".join(
+ textwrap.fill(line)
+ for line in text.split("\n")
+ )
+
+
MSG_COOPERATIVE_CLOSE = _(
"""Your node will negotiate the transaction fee with the remote node. This method of closing the channel usually results in the lowest fees."""
)
### electrum/gui/qml/components/ExceptionDialog.qml
@@ -131,6 +131,7 @@ ElDialog
text: reportText
wrapMode: Text.Wrap
width: parent.width
+ textFormat: Text.RichText
}
}
onClosed: destroy()
### electrum/gui/qml/components/OpenWalletDialog.qml
@@ -46,6 +46,7 @@ ElDialog {
text: Daemon.singlePasswordEnabled || isStartup
? qsTr('Please enter password')
: qsTr('Wallet <b>%1</b> requires password to unlock').arg(name)
+ textFormat: Text.RichText
compact: true
iconStyle: InfoTextArea.IconStyle.Info
backgroundColor: constants.darkerDialogBackground
### electrum/gui/qml/components/Preferences.qml
@@ -369,6 +369,7 @@ Pane {
Label {
Layout.fillWidth: true
text: qsTr('<b>%1%</b> of payment').arg(maxfeeslider._fees[maxfeeslider.value]/10000)
+ textFormat: Text.RichText
wrapMode: Text.Wrap
}
### electrum/gui/qml/components/controls/TxInput.qml
@@ -42,6 +42,7 @@ TextHighlightPane {
: '<' + qsTr('unknown amount') + '>'
font.pixelSize: constants.fontSizeMedium
font.family: FixedFont
+ textFormat: Text.RichText
}
Label {
text: Config.baseUnit
### electrum/gui/qml/components/main.qml
@@ -929,4 +929,22 @@ ApplicationWindow
property var _lastActive: 0 // record time of last activity
property bool _lockDialogShown: false
+ // We want all Text/Label/etc components to use PlainText by default.
+ // Qt normally defaults to AutoText, which allows rich text.
+ // For our Android builds, we patch Qt at compile-time to change this.
+ // (see "qt-6-10-rich-text-should-be-opt-in.patch")
+ // FIXME other platforms? (e.g. running QML on desktop Linux / dev environment)
+ // This runtime check here aims to prevent regressions by hard-failing.
+ Label {
+ id: richtext_sanity_label
+ Component.onCompleted: {
+ if (richtext_sanity_label.textFormat !== 0 && AppController.isAndroid()) {
+ console.log(
+ "richtext_sanity_label failed check: expected PlainText, "
+ + "got " + richtext_sanity_label.textFormat + ". Exiting...")
+ Qt.callLater(Qt.quit)
+ }
+ }
+ }
+
}
### electrum/gui/qml/components/wizard/WCCreateSeed.qml
@@ -49,6 +49,7 @@ WizardComponent {
Layout.fillWidth: true
backgroundColor: constants.darkerDialogBackground
iconStyle: InfoTextArea.IconStyle.Warn
+ textFormat: Text.RichText
}
Label {
### electrum/gui/qml/components/wizard/WCEnterExt.qml
@@ -76,6 +76,7 @@ WizardComponent {
'<br/>',
qsTr('Do not enable it unless you know what it does!'),
].join(' ')
+ textFormat: Text.RichText
}
ElCheckBox {
### electrum/gui/qt/__init__.py
@@ -39,8 +39,8 @@
"you may try 'sudo apt-get install python3-pyqt6'") from e
from PyQt6.QtGui import QGuiApplication, QCursor
-from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog, QToolTip
-from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt
+from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog, QToolTip, QLabel
+from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt, QEvent
import PyQt6.QtCore as QtCore
@@ -113,9 +113,6 @@ def eventFilter(self, obj, event):
class ScreenshotProtectionEventFilter(QObject):
- def __init__(self):
- super().__init__()
-
def eventFilter(self, obj, event):
if (
event.type() == QtCore.QEvent.Type.Show
@@ -126,6 +123,36 @@ def eventFilter(self, obj, event):
return False
+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.
+ FIXME if obj is a QLabel and it contains rich text, has the rich text already been parsed
+ and acted upon by the time 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?
+ E.g. apparently if using a screen reader, the accessibility bridge queries the label’s text
+ from inside setText(), and Qt parses the HTML at that moment. So "Polish" is too late there.
+ In general against parser vulns, it is not even safe to pass untrusted text to the Label()
+ constructor... Instead:
+ lbl = Label(); lbl.setTextFormat(Qt.TextFormat.PlainText); lbl.setText(untrusted_text);
+ should be used... :/
+ """
+ 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
+ if not isinstance(obj, QLabel):
+ return False
+ if obj.textFormat() != Qt.TextFormat.AutoText:
+ # non-default textFormat => we leave it alone
+ return False
+ obj.setTextFormat(Qt.TextFormat.PlainText)
+ return False
+
+
class QElectrumApplication(QApplication):
new_window_signal = pyqtSignal(str, object)
quit_signal = pyqtSignal()
@@ -163,6 +190,8 @@ def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugin
self.screenshot_protection_efilter = ScreenshotProtectionEventFilter()
if sys.platform in ['win32', 'windows'] and self.config.GUI_QT_SCREENSHOT_PROTECTION:
self.app.installEventFilter(self.screenshot_protection_efilter)
+ self.efilter_no_rich_text = InjectNoRichTextEventFilter()
+ self.app.installEventFilter(self.efilter_no_rich_text)
# explicitly set 'AA_DontShowIconsInMenus' False so menu icons are shown on MacOS
self.app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, on=False)
self.app.setWindowIcon(read_QIcon("electrum.png"))
@@ -628,6 +657,7 @@ def standalone_exception_dialog(exception: Union[str, BaseException]) -> None:
app = QApplication([])
msg_box = QMessageBox()
+ msg_box.setTextFormat(Qt.TextFormat.PlainText)
msg_box.setWindowTitle(_("Error starting Electrum"))
msg_box.setIcon(QMessageBox.Icon.Critical)
msg_box.setText(_("An error occurred") + ":")
### electrum/gui/qt/channel_details.py
@@ -3,6 +3,7 @@
import PyQt6.QtGui as QtGui
import PyQt6.QtWidgets as QtWidgets
import PyQt6.QtCore as QtCore
+from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QLabel, QHBoxLayout
from electrum.util import ShortID
@@ -33,6 +34,7 @@ class LinkedLabel(QtWidgets.QLabel):
def __init__(self, text, on_clicked):
super().__init__(text)
self.linkActivated.connect(on_clicked)
+ self.setTextFormat(Qt.TextFormat.RichText)
class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin, QtEventListener):
### electrum/gui/qt/console.py
@@ -37,6 +37,7 @@ def __init__(self, text, parent):
self.setMargin(0)
parent.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWordWrap(True)
+ self.setTextFormat(Qt.TextFormat.RichText)
def mousePressEvent(self, e):
self.hide()
### electrum/gui/qt/exception_window.py
@@ -60,6 +60,7 @@ def __init__(self, config: 'SimpleConfig', exctype, value, tb):
main_box = QVBoxLayout()
heading = QLabel('<h2>' + BaseCrashReporter.CRASH_TITLE + '</h2>')
+ heading.setTextFormat(Qt.TextFormat.RichText)
main_box.addWidget(heading)
main_box.addWidget(QLabel(BaseCrashReporter.CRASH_MESSAGE))
@@ -210,7 +211,7 @@ def __init__(self, *, parent: QWidget, text: str):
report_text = QLabel(text)
report_text.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
- report_text.setTextFormat(Qt.TextFormat.AutoText) # likely rich text
+ report_text.setTextFormat(Qt.TextFormat.RichText)
scroll_area.setWidget(report_text)
vbox.addWidget(scroll_area)
### electrum/gui/qt/history_list.py
@@ -627,7 +627,9 @@ def show_summary(self):
d.setMinimumSize(600, 150)
vbox = QVBoxLayout()
msg = messages.to_rtf(messages.MSG_CAPITAL_GAINS)
- vbox.addWidget(WWLabel(msg))
+ lbl = WWLabel(msg)
+ lbl.setTextFormat(Qt.TextFormat.RichText)
+ vbox.addWidget(lbl)
grid = QGridLayout()
grid.addWidget(QLabel(_("Begin")), 0, 1)
grid.addWidget(QLabel(_("End")), 0, 2)
### electrum/gui/qt/my_treeview.py
@@ -86,7 +86,7 @@ def addConfig(
checked = bool(configvar.get())
tooltip = None
if (long_desc := configvar.get_long_desc()) is not None:
- tooltip = messages.to_rtf(long_desc)
+ tooltip = messages.wrap_multi_paragraph_text(long_desc)
return self.addToggle(
short_desc,
lambda: self._do_toggle_config(configvar, callback=callback),
### electrum/gui/qt/network_dialog.py
@@ -386,7 +386,7 @@ def __init__(self, network: Network, parent=None):
</ul>
"""
)
- grid.addWidget(HelpButton(msg), 0, 4)
+ grid.addWidget(HelpButton(msg, rich_text=True), 0, 4)
grid.addWidget(self.connect_combo, 0, 1, 1, 3)
self.server_e = QLineEdit()
### electrum/gui/qt/password_dialog.py
@@ -121,6 +121,7 @@ def __init__(self, msg, kind, OK_button, wallet=None):
# Password Strength Label
if kind != PW_PASSPHRASE:
self.pw_strength = QLabel()
+ self.pw_strength.setTextFormat(Qt.TextFormat.RichText)
grid.addWidget(self.pw_strength, 3, 0, 1, 2)
self.new_pw.textChanged.connect(self.pw_changed)
@@ -293,7 +294,7 @@ def __init__(self, parent=None, msg=None):
msg = msg or _('Please enter your password')
WindowModalDialog.__init__(self, parent, _("Enter Password"))
self.pw = pw = PasswordLineEdit()
- label = QLabel(msg)
+ self.label = label = QLabel(msg)
label.setWordWrap(True)
vbox = QVBoxLayout()
vbox.addWidget(label)
### electrum/gui/qt/plugins_dialog.py
@@ -50,7 +50,7 @@ def __init__(self, name, metadata, status_button: Optional['PluginStatusButton']
name_label.setIcon(icon)
vbox.addWidget(name_label)
vbox.addStretch()
- vbox.addWidget(WWLabel(description))
+ vbox.addWidget(WWLabel(description)) # must be plain text: don't parse untrusted text as rich-text
vbox.addStretch()
form = QFormLayout(None)
if author:
@@ -196,7 +196,7 @@ def get_plugins_privkey(self) -> Optional['ECPrivkey']:
self.init_plugins_password()
return None
# ask for url and password, same window
- pw = self.password_dialog(msg=messages.MSG_THIRD_PARTY_PLUGIN_WARNING)
+ pw = self.password_dialog(msg=messages.MSG_THIRD_PARTY_PLUGIN_WARNING, rich_text=True)
if not pw:
return None
privkey = self.plugins.derive_privkey(pw, salt)
### electrum/gui/qt/seed_dialog.py
@@ -180,8 +180,10 @@ def __init__(
vbox.addStretch(1)
self.seed_status = WWLabel('')
+ self.seed_status.setTextFormat(Qt.TextFormat.RichText)
vbox.addWidget(self.seed_status)
self.seed_warning = WWLabel('')
+ self.seed_warning.setTextFormat(Qt.TextFormat.RichText)
if msg:
self.seed_warning.setText(seed_warning_msg(seed))
else:
### electrum/gui/qt/settings_dialog.py
@@ -50,7 +50,8 @@ def checkbox_from_configvar(cv: 'ConfigVarWithConfig') -> QCheckBox:
assert short_desc is not None, f"short_desc missing for {cv}"
cb = QCheckBox(short_desc)
if (long_desc := cv.get_long_desc()) is not None:
- cb.setToolTip(messages.to_rtf(long_desc))
+ long_desc = messages.wrap_multi_paragraph_text(long_desc)
+ cb.setToolTip(long_desc)
return cb
### electrum/gui/qt/transaction_dialog.py
@@ -1019,6 +1019,7 @@ def add_tx_stats(self, vbox):
fee_hbox = QHBoxLayout()
self.fee_label = TxDetailLabel()
+ self.fee_label.setTextFormat(Qt.TextFormat.RichText)
fee_hbox.addWidget(self.fee_label)
self.fee_warning_icon = QLabel()
pixmap = QPixmap(icon_path("warning"))
@@ -1121,6 +1122,7 @@ def __init__(
font.setPointSize(font.pointSize() - 1)
self.legend_label.setFont(font)
self.legend_label.setVisible(False)
+ self.legend_label.setTextFormat(Qt.TextFormat.RichText)
self.text_char_format = QTextCharFormat()
self.text_char_format.setBackground(QBrush(self.color))
self.text_char_format.setToolTip(tooltip)
### electrum/gui/qt/update_checker.py
@@ -35,9 +35,11 @@ def __init__(self, *, latest_version=None):
self.content.setContentsMargins(*[10]*4)
self.heading_label = QLabel()
+ self.heading_label.setTextFormat(Qt.TextFormat.RichText)
self.content.addWidget(self.heading_label)
self.detail_label = QLabel()
+ self.detail_label.setTextFormat(Qt.TextFormat.RichText)
self.detail_label.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse)
self.detail_label.setOpenExternalLinks(True)
self.content.addWidget(self.detail_label)
### electrum/gui/qt/util.py
@@ -120,6 +120,7 @@ def __init__(self, text='', parent=None):
WWLabel.__init__(self, text, parent)
self.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.setOpenExternalLinks(True)
+ self.setTextFormat(Qt.TextFormat.RichText)
class AmountLabel(QLabel):
@@ -146,9 +147,10 @@ def setVisible(self, visible):
class HelpMixin:
- def __init__(self, help_text: str, *, help_title: str | None = None):
+ def __init__(self, help_text: str, *, help_title: str | None = None, rich_text: bool = False):
assert isinstance(self, QWidget), "HelpMixin must be a QWidget instance!"
self.help_text = help_text
+ self.rich_text = rich_text
self._help_title = help_title or _('Help')
if isinstance(self, QLabel):
self.setTextInteractionFlags(
@@ -161,15 +163,15 @@ def show_help(self):
parent=self,
title=self._help_title,
text=self.help_text,
- rich_text=True,
+ rich_text=self.rich_text,
)
class HelpLabel(HelpMixin, QLabel):
- def __init__(self, text: str, help_text: str):
+ def __init__(self, text: str, help_text: str, *, rich_text: bool = False):
QLabel.__init__(self, text)
- HelpMixin.__init__(self, help_text)
+ HelpMixin.__init__(self, help_text, rich_text=rich_text)
self.app = QCoreApplication.instance()
self.font = self.font()
@@ -194,19 +196,19 @@ def leaveEvent(self, event):
class HelpButton(HelpMixin, QToolButton):
- def __init__(self, text: str):
+ def __init__(self, text: str, *, rich_text: bool = False):
QToolButton.__init__(self)
- HelpMixin.__init__(self, text)
+ HelpMixin.__init__(self, text, rich_text=rich_text)
self.setText('?')
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.setFixedWidth(round(2.2 * char_width_in_lineedit()))
self.clicked.connect(self.show_help)
class InfoButton(HelpMixin, QPushButton):
- def __init__(self, text: str):
+ def __init__(self, text: str, *, rich_text: bool = False):
QPushButton.__init__(self, _('Info'))
- HelpMixin.__init__(self, text, help_title=_('Info'))
+ HelpMixin.__init__(self, text, help_title=_('Info'), rich_text=rich_text)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.setFixedWidth(6 * char_width_in_lineedit())
self.clicked.connect(self.show_help)
@@ -343,10 +345,18 @@ def query_choice(
return None
return choice_widget.selected_key
- def password_dialog(self, msg=None, parent=None):
+ def password_dialog(
+ self,
+ *,
+ msg: str | None = None,
+ parent: QWidget | None = None,
+ rich_text: bool = False,
+ ):
from .password_dialog import PasswordDialog
parent = parent or self
- d = PasswordDialog(parent, msg)
+ d = PasswordDialog(parent=parent, msg=msg)
+ if rich_text:
+ d.label.setTextFormat(Qt.TextFormat.RichText)
return d.run()
@@ -373,24 +383,21 @@ def custom_message_box(
else:
custom_buttons.append(button)
if type(icon) is QPixmap:
- d = QMessageBox(QMessageBox.Icon.Information, title, str(text), standard_buttons, parent)
+ d = QMessageBox(QMessageBox.Icon.Information, title, "", standard_buttons, parent)
d.setIconPixmap(icon)
else:
- d = QMessageBox(icon, title, str(text), standard_buttons, parent)
+ d = QMessageBox(icon, title, "", standard_buttons, parent)
for button, role, _ in custom_buttons:
d.addButton(button, role)
d.setWindowModality(Qt.WindowModality.WindowModal)
d.setDefaultButton(defaultButton)
if rich_text:
d.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.LinksAccessibleByMouse)
- # set AutoText instead of RichText
- # AutoText lets Qt figure out whether to render as rich text.
- # e.g. if text is actually plain text and uses "\n" newlines;
- # and we set RichText here, newlines would be swallowed
- d.setTextFormat(Qt.TextFormat.AutoText)
+ d.setTextFormat(Qt.TextFormat.RichText)
else:
d.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
d.setTextFormat(Qt.TextFormat.PlainText)
+ d.setText(str(text)) # set the text only after setting textFormat, so unwanted rich text is left unparsed
if checkbox is not None:
d.setCheckBox(checkbox)
result = d.exec()
### electrum/gui/qt/wizard/wizard.py
@@ -39,6 +39,7 @@ def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', *, start
self.setMinimumSize(600, 400)
self.title = QLabel()
+ self.title.setTextFormat(Qt.TextFormat.RichText)
self.window_title = ''
self.finish_label = _('Finish')
### electrum/plugins/coldcard/qt.py
@@ -193,6 +193,7 @@ def connect_and_doit():
for row_num, (member_name, label) in enumerate(rows):
# XXX we know xfp already, even if not connected
widget = QLabel('<tt>000000000000')
+ widget.setTextFormat(Qt.TextFormat.RichText)
widget.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
grid.addWidget(QLabel(label), y, 0, 1, 1, Qt.AlignmentFlag.AlignRight)
### electrum/plugins/revealer/qt.py
@@ -364,7 +364,9 @@ def cypherseed_dialog(self, window):
self.custom_secret_maximum_characters_warning_label = QLabel("<font color='red'>"
+ _("This version supports a maximum of {} characters.").format(self.MAX_PLAINTEXT_LEN)
+"</font>")
+ self.custom_secret_maximum_characters_warning_label.setTextFormat(Qt.TextFormat.RichText)
one_time_pad_warning_label = QLabel("<b>" + _("Warning ") + "</b>: " + _("each Revealer is a one-time-pad, use it for a single secret."))
+ one_time_pad_warning_label.setTextFormat(Qt.TextFormat.RichText)
# Allow users to select text in the labels.
ready_to_encrypt_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
@@ -853,9 +855,10 @@ def calibration_dialog(self, window):
d.setMinimumSize(100, 200)
vbox = QVBoxLayout(d)
- vbox.addWidget(QLabel(''.join(["<br/>", _("If you have an old printer, or want optimal precision"),"<br/>",
- _("print the calibration pdf and follow the instructions "), "<br/>","<br/>",
- ])))
+ vbox.addWidget(QLabel("".join([
+ _("If you have an old printer, or want optimal precision"), "\n",
+ _("print the calibration pdf and follow the instructions "), "\n\n",
+ ])))
self.calibration_h = self.config.get('calibration_h')
self.calibration_v = self.config.get('calibration_v')
cprint = QPushButton(_("Open calibration pdf"))
### electrum/plugins/timelock_recovery/manifest.json
@@ -1,6 +1,6 @@
{
"fullname": "Timelock Recovery Utility",
- "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",
"author": "orenz0@protonmail.com",
"available_for": ["qt"],
"icon":"timelock_recovery_60.png",
### electrum/plugins/trustedcoin/manifest.json
@@ -1,7 +1,7 @@
{
"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",
"requires_wallet_type": ["2fa"],
"registers_wallet_type": "2fa",
"icon":"trustedcoin-status.png",
### electrum/plugins/trustedcoin/qt.py
@@ -125,7 +125,7 @@ def auth_dialog(self, window):
vbox.addLayout(grid)
msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.')
label = QLabel(msg)
- label.setWordWrap(1)
+ label.setWordWrap(True)
vbox.addWidget(label)
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
if not d.exec():
@@ -186,7 +186,8 @@ def show_settings_dialog(self, window, success):
msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\
+ _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>"
label = QLabel(msg)
- label.setOpenExternalLinks(1)
+ label.setOpenExternalLinks(True)
+ label.setTextFormat(Qt.TextFormat.RichText)
hbox.addStretch(10)
hbox.addWidget(logo)
@@ -197,9 +198,11 @@ def show_settings_dialog(self, window, success):
vbox.addLayout(hbox)
vbox.addStretch(10)
- msg = _('TrustedCoin charges a small fee to co-sign transactions. The fee depends on how many prepaid transactions you buy. An extra output is added to your transaction every time you run out of prepaid transactions.') + '<br/>'
+ msg = _('TrustedCoin charges a small fee to co-sign transactions. '
+ 'The fee depends on how many prepaid transactions you buy. '
+ 'An extra output is added to your transaction every time you run out of prepaid transactions.')
label = QLabel(msg)
- label.setWordWrap(1)
+ label.setWordWrap(True)
vbox.addWidget(label)
vbox.addStretch(10)Why this scored 63/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.