Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. QTextEdit detects mouse over word when it's not (previous solution no longer working well enough)
Forum Updated to NodeBB v4.3 + New Features

QTextEdit detects mouse over word when it's not (previous solution no longer working well enough)

Scheduled Pinned Locked Moved Unsolved General and Desktop
3 Posts 2 Posters 456 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • Z Offline
    Z Offline
    ZeHgS
    wrote on last edited by ZeHgS
    #1

    Hi everyone!

    A while ago I made this post: https://forum.qt.io/topic/130637/pyqt5-qtextedit-detects-mouse-over-word-when-it-s-not

    A very helpful and friendly guy over at Stack Overflow provided me with this solution, in line with the suggestion given to me here by user SGaist:

    if self.underMouse():
        pos = mouse_event.pos()
        # create a QTextCursor at that position and select text
        text_cursor = self.cursorForPosition(pos)
        text_cursor.select(QTextCursor.WordUnderCursor)
    
        start = text_cursor.selectionStart()
        end = text_cursor.selectionEnd()
        length = end - start
    
        block = text_cursor.block()
        blockRect = self.document().documentLayout().blockBoundingRect(block)
        # translate by the offset caused by the scroll bar
        blockRect.translate(0, -self.verticalScrollBar().value())
        if not pos in blockRect:
            # clear the selection since the mouse is not over the block
            text_cursor.setPosition(text_cursor.position())
        elif length:
            # ensure that the mouse is actually over a word
            startFromBlock = start - block.position()
            textLine = block.layout().lineForTextPosition(startFromBlock)
            endFromBlock = startFromBlock + length
            x, _ = textLine.cursorToX(endFromBlock)
            if pos.x() > blockRect.x() + x:
                # mouse cursor is not over a word, clear the selection
                text_cursor.setPosition(text_cursor.position())
    

    It worked well enough to make my first editor fully usable. Unfortunately, however, it still kept selecting words that shouldn't be selected. Specifically, whenever the mouse is above/below the actual text either the very first word or the very last gets selected.

    I am making a different editor now and need a way to check whether the mouse position really is on top of the word that is said to have been selected. I tried comparing self.cursorRect() 's position to the mouse's but they didn't seem to match independently of whether I converted from or to global coordinates. Is there a way to be 100% accurate?

    Thanks a lot!

    JonBJ 1 Reply Last reply
    0
    • Z ZeHgS

      Hi everyone!

      A while ago I made this post: https://forum.qt.io/topic/130637/pyqt5-qtextedit-detects-mouse-over-word-when-it-s-not

      A very helpful and friendly guy over at Stack Overflow provided me with this solution, in line with the suggestion given to me here by user SGaist:

      if self.underMouse():
          pos = mouse_event.pos()
          # create a QTextCursor at that position and select text
          text_cursor = self.cursorForPosition(pos)
          text_cursor.select(QTextCursor.WordUnderCursor)
      
          start = text_cursor.selectionStart()
          end = text_cursor.selectionEnd()
          length = end - start
      
          block = text_cursor.block()
          blockRect = self.document().documentLayout().blockBoundingRect(block)
          # translate by the offset caused by the scroll bar
          blockRect.translate(0, -self.verticalScrollBar().value())
          if not pos in blockRect:
              # clear the selection since the mouse is not over the block
              text_cursor.setPosition(text_cursor.position())
          elif length:
              # ensure that the mouse is actually over a word
              startFromBlock = start - block.position()
              textLine = block.layout().lineForTextPosition(startFromBlock)
              endFromBlock = startFromBlock + length
              x, _ = textLine.cursorToX(endFromBlock)
              if pos.x() > blockRect.x() + x:
                  # mouse cursor is not over a word, clear the selection
                  text_cursor.setPosition(text_cursor.position())
      

      It worked well enough to make my first editor fully usable. Unfortunately, however, it still kept selecting words that shouldn't be selected. Specifically, whenever the mouse is above/below the actual text either the very first word or the very last gets selected.

      I am making a different editor now and need a way to check whether the mouse position really is on top of the word that is said to have been selected. I tried comparing self.cursorRect() 's position to the mouse's but they didn't seem to match independently of whether I converted from or to global coordinates. Is there a way to be 100% accurate?

      Thanks a lot!

      JonBJ Offline
      JonBJ Offline
      JonB
      wrote on last edited by JonB
      #2

      @ZeHgS said in QTextEdit detects mouse over word when it's not (previous solution no longer working well enough):

      Specifically, whenever the mouse is above/below the actual text either the very first word or the very last gets selected.

      I recall somebody asked about just this a while ago. The problem is how to search to spot that thread....

      ...Oh I see, maybe that thread was your own to which you refer!

      I don't claim to understand the specifics of your current solution. But it does seem to me you would need to compare the mouse position against the area occupied by the text if you want to determine whether the mouse lies outside of it.

      I tried comparing self.cursorRect() 's position to the mouse's but they didn't seem to match independently of whether I converted from or to global coordinates. Is there a way to be 100% accurate?

      I see nothing about global coordinates in your code. Maybe show what you tried, and what was wrong about it?

      1 Reply Last reply
      0
      • Z Offline
        Z Offline
        ZeHgS
        wrote on last edited by ZeHgS
        #3

        Thank you very much for replying!

        Please excuse me, it turns out that a modification I made caused the problem. The original version he provided me with seems to work perfectly, at least in isolation: There really was something missing.

        from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit
        from PyQt5.QtGui import QCursor, QColor, QBrush, QTextLayout, QTextCharFormat, QTextCursor
        from PyQt5.Qt import QPoint
        
        
        app = QApplication([])
        
        main_window = QMainWindow()
        
        class HighlightTextEdit(QTextEdit):
            highlightPos = -1, -1
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.setMouseTracking(True)
        
                self.highlightBlock = self.document().firstBlock()
                self.highlightFormat = QTextCharFormat()
                self.highlightFormat.setBackground(
                    QBrush(QColor('#FFFF00')))
                self.highlightFormat.setFontUnderline(True)
        
                self.document().contentsChanged.connect(self.highlight)
        
            def highlight(self, pos=None):
                if not self.toPlainText() or not self.isVisible():
                    return
                if pos is None:
                    pos = self.mapFromGlobal(QCursor.pos())
                cursor = self.cursorForPosition(pos)
                cursor.select(cursor.WordUnderCursor)
        
                start = cursor.selectionStart()
                end = cursor.selectionEnd()
                doc = self.document()
                block = doc.findBlock(start)
        
                # check if the mouse is actually inside the rectangle of the block
                blockRect = doc.documentLayout().blockBoundingRect(block)
                blockLayout = block.layout()
                if not pos in blockRect.translated(0, -self.verticalScrollBar().value()):
                    # mouse is outside of the block, no highlight
                    start = end = -1
        
                startFromBlock = start - block.position()
                length = end - start
                if length:
                    # ensure that the cursor is actually within the boundaries of the word
                    textLine = blockLayout.lineForTextPosition(startFromBlock)
                    endFromBlock = startFromBlock + length
                    x, _ = textLine.cursorToX(endFromBlock)
                    if pos.x() > blockRect.x() + x:
                        start = end = -1
                        length = 0
        
                # if the range is the same as the previous call, we can ignore it
                if self.highlightPos == (start, end):
                    return
        
                # clear the previous highlighting
                self.highlightBlock.layout().clearFormats()
                self.highlightPos = start, end
                if length:
                    # create a FormatRange for the highlight using the current format
                    r = QTextLayout.FormatRange()
                    r.start = startFromBlock
                    r.length = length
                    r.format = self.highlightFormat
                    blockLayout.setFormats([r])
        
                # notify that the document must be layed out (and repainted) again
                dirtyEnd = max(
                    self.highlightBlock.position() + self.highlightBlock.length(),
                    block.position() + block.length()
                )
                dirtyStart = min(self.highlightBlock.position(), block.position())
                doc.markContentsDirty(dirtyStart, dirtyEnd - dirtyStart)
                self.highlightBlock = block
        
            def viewportEvent(self, event):
                if event.type() == event.Leave:
                    # disable highlight when leaving, using coordinates outside of the
                    # viewport to ensure that highlighting is cleared
                    self.highlight(QPoint(-1, -1))
                elif event.type() == event.Enter:
                    self.highlight()
                elif event.type() == event.MouseMove:
                    if not event.buttons():
                        self.highlight(event.pos())
                elif event.type() == event.MouseButtonRelease:
                    self.highlight(event.pos())
                return super().viewportEvent(event)
        
        text_edit = HighlightTextEdit()
        text_edit.setText("Óleo de motor Gás Racing Team Marca Vintage Sinais de metal Garage Man Cave Acessórios de decoração de parede Placas de metal retrô adesivos de parede")
        
        main_window.setCentralWidget(text_edit)
        main_window.show()
        
        text_edit.setFocus()
        
        app.exec()
        

        I'll just rollback to this one. Thanks a lot and sorry for the trouble!

        EDIT.: I came up with a better check for my use case:

        def check_if_mouse_position_is_over_text(self, position):
                if self.underMouse():
                    pos = position
                    # create a QTextCursor at that position and select text
                    text_cursor = self.cursorForPosition(pos)
                    text_cursor.select(text_cursor.WordUnderCursor)
                    selected_text = text_cursor.selectedText()
        
                    start = text_cursor.selectionStart()
                    end = text_cursor.selectionEnd()
                    length = end - start
                    text_cursor.clearSelection()
        
                    block = text_cursor.block()
                    block_rect = self.document().documentLayout().blockBoundingRect(block)
                    # translate by the offset caused by the scroll bar
                    block_rect.translate(0, -self.verticalScrollBar().value())
        
                    if length:
                        new_cursor = self.textCursor()
                        new_cursor.setPosition(start)
                        self.setTextCursor(new_cursor)
                        start_rect = self.cursorRect()
                        start_point = self.mapToGlobal(start_rect.topLeft())
        
                        new_cursor.setPosition(end)
                        self.setTextCursor(new_cursor)
                        end_rect = self.cursorRect()
                        end_point = self.mapToGlobal(end_rect.topLeft())
        
                        new_cursor.clearSelection()
        
                        start_point_within_block = start - block.position()
                        text_line = block.layout().lineForTextPosition(start_point_within_block)
                        end_point_within_block = start_point_within_block + length
                        x_end, _ = text_line.cursorToX(end_point_within_block)
        
                        # extends the acceptable margin by 15 on both sides because Qt's falls slightly short of the real one
                        error_margin = 15
        
                        if pos.x() + error_margin >= start_point.x() and pos.x() - error_margin <= end_point.x():
                            return True
        
                return False
        
        1 Reply Last reply
        2

        • Login

        • Login or register to search.
        • First post
          Last post
        0
        • Categories
        • Recent
        • Tags
        • Popular
        • Users
        • Groups
        • Search
        • Get Qt Extensions
        • Unsolved