Search code examples
pythonpyqtpyqt5qtextbrowser

How to make text in QTextBrowser selectable?


I want to be able to select a piece of text in a non-custom way, the way you click on a website link in the middle of a text ( "Hello my name is www.google.com", www.google.com isn't highlighted when you press on it. When you press on it, it takes you to the website the same i want with my text. I want to make it like " hello my name is Jeff, i live in London, i eat potato every day" i want the user to be able to select each piece of sentence alone ("hello my name is Jeff,") alone, ("i live in London,") alone, so when the user moves the mouse cursor on a sentence it goes highlighted (like being ready to be selected) and after that i want to add some functionality to it.

Here is a similar project check the upper text, not the lower text and how it reacts with the mouse.

http://quran.ksu.edu.sa/tafseer/tabary/sura10-aya18.html


Solution

  • Here is a start. In the code below, the clickable phrases in the text are translated to a ordered list of text cursors. When the mouse is moved (i.e. when a mouseMove event occurs) the position in the text under the mouse pointer is compared to this list of text cursors. If this position falls within the bounds of a cursor in the list, the corresponding phrase is highlighted by setting the extra selections of the browser. When a mouse button is clicked and the mouse is over a clickable phrase, a signal is emitted containing a text cursor corresponding to the selected phrase in the text. This signal can then be connected to a slot of another widget for further actions (like opening a message box like in the example below).

    from PyQt5.QtCore import pyqtSignal, Qt
    from PyQt5.QtWidgets import QTextBrowser, QApplication, QMessageBox
    from PyQt5.QtGui import QFont, QSyntaxHighlighter, QTextCharFormat, QTextCursor
    
    import bisect, re
    
    
    class MyHighlighter(QSyntaxHighlighter):
        def __init__(self, keywords, parent):
            super().__init__(parent)
            self.keywords = keywords
    
        def highlightBlock(self, block):
            if not self.keywords:
                return
            charFormat = QTextCharFormat()
            charFormat.setFontWeight(QFont.Bold)
            charFormat.setForeground(Qt.darkMagenta)
            regex = re.compile('|'.join(self.keywords), re.IGNORECASE)
            result = regex.search(block, 0)
            while result:
                self.setFormat(result.start(),result.end()-result.start(), charFormat)
                result = regex.search(block, result.end())
    
    
    class MyBrowser(QTextBrowser):
        text_clicked = pyqtSignal("QTextCursor")
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setMouseTracking(True)
            self.setTextInteractionFlags(Qt.NoTextInteraction)
    
            # self.phrases contains all phrases that should be clickable.
            self.phrases = set()
            self.cursors = []
    
            # ExtraSelection object for highlighting phrases under the mouse cursor
            self.selection = QTextBrowser.ExtraSelection()
            self.selection.format.setBackground(Qt.blue)
            self.selection.format.setForeground(Qt.white)
            self.selected_cursor = None
    
            # custom highlighter for highlighting all phrases
            self.highlighter = MyHighlighter(self.phrases, self)
            self.document().contentsChange.connect(self.text_has_changed)
    
        @property
        def selected_cursor(self):
            return None if self.selection.cursor == QTextCursor() else self.selection.cursor
    
        @selected_cursor.setter
        def selected_cursor(self, cursor):
            if cursor is None:
                cursor = QTextCursor()
            if self.selection.cursor != cursor:
                self.selection.cursor = cursor
                self.setExtraSelections([self.selection])
    
        def mouseMoveEvent(self, event):
            ''' Update currently selected cursor '''
            cursor = self.cursorForPosition(event.pos())
            self.selected_cursor = self.find_selected_cursor(cursor)
    
        def mouseReleaseEvent(self, event):
            ''' Emit self.selected_cursor signal when currently hovering over selectable phrase'''
            if self.selected_cursor:
                self.text_clicked.emit(self.selected_cursor)
                self.selected_cursor = None
    
        def add_phrase(self, phrase):
            ''' Add phrase to set of phrases and update list of text cursors'''
            if phrase not in self.phrases:
                self.phrases.add(phrase)
                self.find_cursors(phrase)
                self.highlighter.rehighlight()
    
        def find_cursors(self, phrase):
            ''' Find all occurrences of phrase in the current document and add corresponding text cursor
            to self.cursors '''
            if not phrase:
                return
            self.moveCursor(self.textCursor().Start)
            while self.find(phrase):
                cursor = self.textCursor()
                bisect.insort(self.cursors, cursor)
            self.moveCursor(self.textCursor().Start)
    
        def find_selected_cursor(self, cursor):
            ''' return text cursor corresponding to current mouse position or None if mouse not currently
            over selectable phrase'''
            position = cursor.position()
            index = bisect.bisect(self.cursors, cursor)
            if index < len(self.cursors) and self.cursors[index].anchor() <= position:
                return self.cursors[index]
            return None
    
        def text_has_changed(self):
            self.cursors.clear()
            self.selected_cursor = None
            for phrase in self.phrases:
                self.find_cursors(phrase)
                self.highlighter.rehighlight()
    
    
    def text_message(widget):
        def inner(cursor):
            text = cursor.selectedText()
            pos = cursor.selectionStart()
            QMessageBox.information(widget, 'Information',
                    f'You have clicked on the phrase <b>{text}</b><br>'
                    f'which starts at position {pos} in the text')
        return inner
    
    
    if __name__=="__main__":
        app = QApplication([])
        window = MyBrowser()
        window.resize(400,300)
        information = text_message(window)
    
        text = '''
        <h1>Title</h1>
        <p>This is a random text with. The following words are highlighted</p>
        <ul>
        <li>keyword1</li>
        <li>keyword2</li>
        </ul>
        <p>Click on either keyword1 or keyword2 to get more info. 
        '''
    
        window.add_phrase('keyword1')
        window.add_phrase('keyword2')
        window.setText(text)
        window.text_clicked.connect(information)
        window.show()
        app.exec()