QTextEdit detects mouse over word when it's not (previous solution no longer working well enough)
-
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!
-
@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?
-
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