Is there a better way to wordwrap text in QToolTip than just using RegExp?

If the text in a tooltip is rich text, it is automatically word-wrapped.

Here's a trivial example, where setting the font to black makes it "rich text" and so it gets word wrapped. Leaving out the font declarations means the tooltip will be plain text and extend the whole length of the screen.

QString toolTip = QString("<FONT COLOR=black>");
toolTip += ("I am the very model of a modern major general, I've information vegetable animal and mineral, I know the kinges of England and I quote the fights historical from Marathon to Waterloo in order categorical...");
toolTip += QString("</FONT>");
widget->setToolTip(sToolTip);

Of course with this example, the width of the tooltip is up to the platform.

There is a new suggestion in the Qt bug tracker about this problem: https://bugreports.qt.io/browse/QTBUG-41051. It also requests width to be changeable.


The prevailing answers to this question do technically suffice as short-term techno-fixes (also known as: when you just friggin' need tooltips to work as advertised), but fall far short of a general-purpose solution.

What's Wrong with "<font>...</font>", Anyway?

If you don't mind manually embedding all tooltip text across your entire application in HTML tags with possibly harmful side effects (e.g., <font color=black>...</font>) and the inevitable fragility that entails, absolutely nothing. In particular, the manual approach:

  • Invites accidental omissions. Forget to embed even a single tooltip at 3:14AM on yet another crunch weekend? Yeah. That's not gonna wrap. Hope you religiously test by cursory inspection all tooltips or you're gonna have a bad time.
  • Invites accidental collisions with HTML syntax – notably, the reserved &, <, and > characters. Avoiding collisions requires manually preprocessing all tooltip text across your entire application. You can't blindly copy-and-paste tooltip text anymore, because that's no longer guaranteeably safe. Forget to replace even a single & with &amp; in a tooltip during the same all-night code bender? Yeah. That's not gonna parse. Hope you religiously test by cursory inspection all tooltips or you're gonna have a bad time.
  • Fails to scale. Existing Qt applications typically define an obscene number of tooltips. What if yours does? Hope you enjoy globally search-and-replacing all tooltip text in an HTML-safe manner avoiding collision with reserved HTML characters. you won't

We've established that the manual approach is the wrong approach. So, what's the right approach? Is there even such a thing in this bug-encrusted world?

Global Event Filter: The Right Way

Actually, the right way is for The Qt Company (QtC) to fix this friggin' bug already. No, really. It's been over three years. Plaintext tooltips still remain fundamentally broken.

Since fixing this friggin' bug already appears to be infeasible, it's up to each individual Qt application to do so globally for that application by installing an application-wide event filter. For those unfamiliar with Qt event filters, hi! I recommend perusing this free-as-in-beer excerpt from the Event Processing chapter of the sadly outdated text C++ GUI Programming with Qt4. It's aged surprisingly well, considering the radioactive half-life of the average online article.

Everybody leveled up? Let's do this.

Whenever any widget's tooltip is set, the QEvent::ToolTipChange event is fired immediately before that tooltip is actually set. Likewise, installing an event filter on the QApplication-based qApp singleton sends every event for every object in the application to this filter's eventFilter() method before that event is sent anywhere else.

Together, these two observations yield the following general-purpose solution:

  1. Define a new QAwesomeTooltipEventFilter subclass of the stock QObject base class.
  2. In this subclass, override the eventFilter() method to:
    1. If the current event is a QEvent::ToolTipChange event:
      1. Detect whether or not the tooltip set by this event is:
        • Rich text (i.e., contains one or more HTML-like rich text tags).
        • Plaintext (i.e., contains no such tags).
      2. If this tooltip is plaintext:
        1. Escape all conflicting HTML syntax in this tooltip (e.g., by globally replacing all & characters with &amp; substrings).
        2. Embed this tooltip in the safest possible HTML-like rich text tag guaranteed to always reduce to a noop (i.e., <qt>...</qt>).
        3. Set this widget's tooltip to that text.
        4. Cease handling this event.
    2. Else, propagate rather than handle that event – ensuring that rich text tooltips and all other events remain untouched.
  3. Pass an instance of this new subclass to the QApplication::installEventFilter() method immediately after creating the QApplication singleton for your application.

Because Qt unconditionally wraps rich text tooltips, globally converting all plaintext to rich text tooltips in this manner correctly wraps all tooltips without requiring even a single line of manual intervention. ...a little applause, maybe?

A Little Code, Maybe?

The requisite code depends on whether or not your application is:

  • C++. In this case, I advise stripmining the official bitcoin GUI for inspiration. Antiquated C++ exceeds my volunteer mandate here, because I am lazy. In particular, you want:
    • The ToolTipToRichTextFilter subclass defined by the:
      • guiutil.h header.
      • guiutil.cpp implementation.
    • The global installation of this event filter in the main bitcoin.cpp implementation.
  • Python. In this case, this is your lucky StackOverflow day.

The following Python-based event filter assumes use of the PySide2 Qt bindings, because... reasons. In theory, rewriting this filter for alternate Qt bindings (e.g., PyQt5, PySide) should reduce to swapping out all PySide2 substrings for your preferred binding names.

Lastly, I like copious reStructuredText (reST) documentation – especially for non-trivial monkey patches to core Qt behaviour like this. If you don't, you know what to do with it.

import html
from PySide2.QtCore import Qt, QEvent, QObject
from PySide2.QtWidgets import QWidget

class QAwesomeTooltipEventFilter(QObject):
    '''
    Tooltip-specific event filter dramatically improving the tooltips of all
    widgets for which this filter is installed.

    Motivation
    ----------
    **Rich text tooltips** (i.e., tooltips containing one or more HTML-like
    tags) are implicitly wrapped by Qt to the width of their parent windows and
    hence typically behave as expected.

    **Plaintext tooltips** (i.e., tooltips containing no such tags), however,
    are not. For unclear reasons, plaintext tooltips are implicitly truncated to
    the width of their parent windows. The only means of circumventing this
    obscure constraint is to manually inject newlines at the appropriate
    80-character boundaries of such tooltips -- which has the distinct
    disadvantage of failing to scale to edge-case display and device
    environments (e.g., high-DPI). Such tooltips *cannot* be guaranteed to be
    legible in the general case and hence are blatantly broken under *all* Qt
    versions to date. This is a `well-known long-standing issue <issue_>`__ for
    which no official resolution exists.

    This filter globally addresses this issue by implicitly converting *all*
    intercepted plaintext tooltips into rich text tooltips in a general-purpose
    manner, thus wrapping the former exactly like the latter. To do so, this
    filter (in order):

    #. Auto-detects whether the:

       * Current event is a :class:`QEvent.ToolTipChange` event.
       * Current widget has a **non-empty plaintext tooltip**.

    #. When these conditions are satisfied:

       #. Escapes all HTML syntax in this tooltip (e.g., converting all ``&``
          characters to ``&amp;`` substrings).
       #. Embeds this tooltip in the Qt-specific ``<qt>...</qt>`` tag, thus
          implicitly converting this plaintext tooltip into a rich text tooltip.

    .. _issue:
        https://bugreports.qt.io/browse/QTBUG-41051
    '''


    def eventFilter(self, widget: QObject, event: QEvent) -> bool:
        '''
        Tooltip-specific event filter handling the passed Qt object and event.
        '''

        # If this is a tooltip event...
        if event.type() == QEvent.ToolTipChange:
            # If the target Qt object containing this tooltip is *NOT* a widget,
            # raise a human-readable exception. While this should *NEVER* be the
            # case, edge cases are edge cases because they sometimes happen.
            if not isinstance(widget, QWidget):
                raise ValueError('QObject "{}" not a widget.'.format(widget))

            # Tooltip for this widget if any *OR* the empty string otherwise.
            tooltip = widget.toolTip()

            # If this tooltip is both non-empty and not already rich text...
            if tooltip and not Qt.mightBeRichText(tooltip):
                # Convert this plaintext tooltip into a rich text tooltip by:
                #
                #* Escaping all HTML syntax in this tooltip.
                #* Embedding this tooltip in the Qt-specific "<qt>...</qt>" tag.
                tooltip = '<qt>{}</qt>'.format(html.escape(tooltip))

                # Replace this widget's non-working plaintext tooltip with this
                # working rich text tooltip.
                widget.setToolTip(tooltip)

                # Notify the parent event handler this event has been handled.
                return True

        # Else, defer to the default superclass handling of this event.
        return super().eventFilter(widget, event)

Globally installing this event filter then reduces to the following two-liner:

from PySide2.QtGui import qApp
qApp.installEventFilter(QAwesomeTooltipEventFilter(qApp))

Voilà! Globally sane tooltips in twenty lines of Python. What? It's twenty if you squint.

What's the Catch?

Depending on the exact Qt binding you use, the Qt.mightBeRichText() utility function called above may not actually be defined (presumably due to binding parser issues). If this is the sad case for your binding, your simplest solution is to either:

  • Assume all tooltips are plaintext and just remove the call to Qt.mightBeRichText(). not smart
  • Scan this tooltip for HTML-like tags, presumably with a regular expression heuristic. smart but exceeds my laziness threshold

We're Done Here.

Tags:

C++

Qt