1

I have implemented very simple syntax highlighter and I'm using it with the QTextEdit.

class MyHighlighter(QtGui.QSyntaxHighlighter):

    def __init__(self, parent):
        QtGui.QSyntaxHighlighter.__init__(self, parent)

        self.Rules = []

        classFormat = QtGui.QTextCharFormat()
        classFormat.setFontWeight(QtGui.QFont.Bold)
        classFormat.setForeground(QtCore.Qt.darkMagenta)
        classFormat.setToolTip("this is very important!")

        self.Rules.append(
                ('keyword', classFormat)
            )

    def highlightBlock(self, text):

        for pattern, classFormat in self.Rules:
            expression = re.compile(pattern)
            for match in re.finditer(expression, text):
                index = match.start()
                length = match.end() - index
                self.setFormat(index, length, classFormat)

Syntax highlighter correctly set text formatting but the tooltip isn't available. It simply never visible.

I found some a old bug report which describe a similar behaviour but looks there is no solution for mentioned issue: https://bugreports.qt.io/browse/QTBUG-21553

How can I workaround this to get the tool tip working?

I was thinking that I can use html tags inside the QTextEdit. But I don't like that idea as it will add more complexity to text preprocessing (I'm working on big files). Also did some experiments with that and looks like it also could be tricky.

Adam
  • 2,254
  • 3
  • 24
  • 42
  • Qt only [supports a limited subset of html](https://doc.qt.io/qt-5/richtext-html-subset.html) in `QTextEdit`, so I don't see how html tags are going to help. Is this bug still present in Qt5? If so, you will have to implement your own tooltip functionality, based on [cursorForPosition](https://doc.qt.io/qt-5/qtextedit.html#cursorForPosition). In fact, that might be preferrable anyway, since it will give you more control over the placement of the tooltip and so forth. – ekhumoro Jan 04 '19 at 22:08
  • For example I could try to use span with title attribute. I have tried and it works, but still there was some issues with this. But is still present in Qt5, I'm using this version. Thank you for the suggestion about cursorForPosition. I will check it. – Adam Jan 04 '19 at 22:30
  • Ah, it seems that Qt implements the HTML4 spec, so the [title attribute](https://www.w3.org/TR/html401/struct/global.html#h-7.4.3) is supported. – ekhumoro Jan 04 '19 at 22:43

1 Answers1

0

From what I remember in the bug tracker, the problem is that the widget's tooltip lookup doesn't get updated when the highlighter runs.

You can reimplement the tooltip lookup yourself like this (chopped down from some of my own code which uses QPlainTextEdit but I've confirmed to function the same if you %s/QPlainTextEdit/QTextEdit/g):

from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat, QTextCursor
from PyQt5.QtWidgets import QApplication, QPlainTextEdit, QToolTip

from nlprule import Tokenizer, Rules


class TooltipPlainTextEdit(QPlainTextEdit):
    def __init__(self, *args):
        QPlainTextEdit.__init__(self, *args)
        self.setMouseTracking(True)
        self.highlighter = NlpRuleHighlighter(self.document())

    def event(self, event) -> bool:
        """Reimplement tooltip lookup to get nlprule messages working"""
        if event.type() == QEvent.ToolTip:
            pos = event.pos()
            pos.setX(pos.x() - self.viewportMargins().left())
            pos.setY(pos.y() - self.viewportMargins().top())
            cursor = self.cursorForPosition(pos)

            # QTextCursor doesn't have a quicker way to get
            # highlighter-applied formats
            for fmt in cursor.block().layout().formats():
                if (fmt.start <= cursor.position() and
                        fmt.start + fmt.length >= cursor.position()):

                    cursor.setPosition(fmt.start)
                    cursor.setPosition(fmt.start + fmt.length,
                                       QTextCursor.KeepAnchor)
                    QToolTip.showText(event.globalPos(), fmt.format.toolTip(),
                        self, self.cursorRect(cursor))
                    return True

        return super(TooltipPlainTextEdit, self).event(event)


class NlpRuleHighlighter(QSyntaxHighlighter):
    grammar_format = QTextCharFormat()
    grammar_format.setUnderlineColor(Qt.blue)
    grammar_format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)

    def __init__(self, *args):
        QSyntaxHighlighter.__init__(self, *args)

        self.nlprule_tokenizer = Tokenizer.load("en")
        self.nlprule_rules = Rules.load("en", self.nlprule_tokenizer)

    def highlightBlock(self, text: str):
        for sugg in self.nlprule_rules.suggest(text):
            self.grammar_format.setToolTip(sugg.message)
            self.setFormat(sugg.start, sugg.end - sugg.start,
                           self.grammar_format)


if __name__ == '__main__':  # pragma: nocover
    import sys

    app = QApplication(sys.argv)
    edit = TooltipPlainTextEdit()
    edit.show()

    sys.exit(app.exec_())

In my preliminary "don't do more refactoring than I need to" testing, I found that this does preserve and assign the distinct messages like "Consider using the plural form here" and "Did you mean to have?" correctly, so I can only assume that QSyntaxHighlighter::setFormat makes a copy of self.grammar_format's state rather than taking a reference.

eyllanesc implemented something similar first, but he didn't actually do the lookup of what tooltip to display (thanks to user_none on QtCenter for pointing me in the right direction) and I didn't like how he was doing wordwise-select rather than working off the extents of the highlight. (Funny enough, in doing so, I answered the question in user7179690's comment here.)

I also did the margin correction that namezero pointed out the need for.

ssokolow
  • 14,938
  • 7
  • 52
  • 57