Issue
I am experiencing an odd bug. A scrollbar thinks its QTextEdit is smaller than it actually is!
I have a custom QTextEdit. Whenever I replace the HTML, the scrollbar gets reset to 0. To overcome this, I just override setHtml, store the old value and reset it after replacing the HTML.
The trouble is, if my window is larger than its minimum size (or starting, if there is no minimum), the QTextEdit grows as expected but this solution stops working for scrollbar position values above a certain number, meaning the QTextEdit still thinks it's the same size as the minimum even if I start the program maximized or resize it manually so anything that would not have been displayed with the window at its minimum size gets cut off from the scrollbar.
I tried setting all containers to all size policies and the only ones that "worked" were Maximum and Fixed but then the QTextEdit no longer resized with the window as I need it to. Setting the slider position directly instead didn't work either.
Minimum reproducible example:
from PyQt5.QtWidgets import QMainWindow, QApplication, QTextEdit, QWidget, QScrollArea, QVBoxLayout, QHBoxLayout, QSizePolicy
from PyQt5.QtGui import QMouseEvent, QTextCursor, QIcon
from PyQt5.QtCore import Qt
import GUIMainWindow
from itertools import chain # to unnest nested lists
import re
application = QApplication([])
class GUIMainWindow(QMainWindow):
def __init__(self):
super().__init__()
# set the Window's minimum size according to the width of images in the product gallery plus a constant
self.setMinimumSize(800, 600)
# set up the main window
self.setGeometry(0, 30, 800, 600)
def set_up_window(self):
# the main container is a vertical scroll area, with the image section on top and the info section on the bottom
main_container = QWidget()
main_container.setMouseTracking(True)
main_container_layout = QVBoxLayout()
main_container.setLayout(main_container_layout)
# add the description editor
description_editor = TextEditor()
description_editor.setAcceptRichText(True)
description_editor.setTextInteractionFlags(Qt.NoTextInteraction)
description_editor.setText(
'Meditation has proven difficult to define as it covers a wide range of dissimilar practices in different traditions. In popular usage, the word "meditation" and the phrase "meditative practice" are often used imprecisely to designate practices found across many cultures.[4][20] These can include almost anything that is claimed to train the attention of mind or to teach calm or compassion.[21] There remains no definition of necessary and sufficient criteria for meditation that has achieved universal or widespread acceptance within the modern scientific community. In 1971, Claudio Naranjo noted that "The word "meditation" has been used to designate a variety of practices that differ enough from one another so that we may find trouble in defining what meditation is."[22]: 6 A 2009 study noted a "persistent lack of consensus in the literature" and a "seeming intractability of defining meditation".[23]: 135 <BR><BR>'
'In modern psychological research, meditation has been defined and characterized in a variety of ways. Many of these emphasize the role of attention[4][1][2][3] and characterize the practice of meditation as attempts to get beyond the reflexive, "discursive thinking"[note 1] or "logic"[note 2] mind[note 3] to achieve a deeper, more devout, or more relaxed state.<BR><BR>'
'Bond et al. (2009) identified criteria for defining a practice as meditation "for use in a comprehensive systematic review of the therapeutic use of meditation", using "a 5-round Delphi study with a panel of 7 experts in meditation research" who were also trained in diverse but empirically highly studied (Eastern-derived or clinical) forms of meditation[note 4]:<BR><BR>'
'three main criteria [...] as essential to any meditation practice: the use of a defined technique, logic relaxation,[note 5] and a self-induced state/mode.<BR><BR>'
'Other criteria deemed important [but not essential] involve a state of psychophysical relaxation, the use of a self-focus skill or anchor, the presence of a state of suspension of logical thought processes, a religious/spiritual/philosophical context, or a state of mental silence.[23]: 135 .<BR><BR>'
'[...] It is plausible that meditation is best thought of as a natural category of techniques best captured by "family resemblances" [...] or by the related "prototype" model of concepts."[23]: 135 [note 6].<BR><BR>'
'Several other definitions of meditation have been used by influential modern reviews of research on meditation across multiple traditions:[note 7].<BR><BR>'
'Walsh & Shapiro (2006): "[M]editation refers to a family of self-regulation practices that focus on training attention and awareness in order to bring mental processes under greater voluntary control and thereby foster general mental well-being and development and/or specific capacities such as calm, clarity, and concentration"[1]: 228–29 .<BR>'
'Cahn & Polich (2006): "[M]editation is used to describe practices that self-regulate the body and mind, thereby affecting mental events by engaging a specific attentional set.... regulation of attention is the central commonality across the many divergent methods"[2]: 180 .<BR>'
'Jevning et al. (1992): "We define meditation... as a stylized mental technique... repetitively practiced for the purpose of attaining a subjective experience that is frequently described as very restful, silent, and of heightened alertness, often characterized as blissful"[3]: 415 .<BR>'
'Goleman (1988): "the need for the meditator to retrain his attention, whether through concentration or mindfulness, is the single invariant ingredient in... every meditation system"[4]: 107 '
'Separation of technique from tradition.<BR>''Meditation has proven difficult to define as it covers a wide range of dissimilar practices in different traditions. In popular usage, the word "meditation" and the phrase "meditative practice" are often used imprecisely to designate practices found across many cultures.[4][20] These can include almost anything that is claimed to train the attention of mind or to teach calm or compassion.[21] There remains no definition of necessary and sufficient criteria for meditation that has achieved universal or widespread acceptance within the modern scientific community. In 1971, Claudio Naranjo noted that "The word "meditation" has been used to designate a variety of practices that differ enough from one another so that we may find trouble in defining what meditation is."[22]: 6 A 2009 study noted a "persistent lack of consensus in the literature" and a "seeming intractability of defining meditation".[23]: 135 <BR><BR>'
'In modern psychological research, meditation has been defined and characterized in a variety of ways. Many of these emphasize the role of attention[4][1][2][3] and characterize the practice of meditation as attempts to get beyond the reflexive, "discursive thinking"[note 1] or "logic"[note 2] mind[note 3] to achieve a deeper, more devout, or more relaxed state.<BR><BR>'
'Bond et al. (2009) identified criteria for defining a practice as meditation "for use in a comprehensive systematic review of the therapeutic use of meditation", using "a 5-round Delphi study with a panel of 7 experts in meditation research" who were also trained in diverse but empirically highly studied (Eastern-derived or clinical) forms of meditation[note 4]:<BR><BR>'
'three main criteria [...] as essential to any meditation practice: the use of a defined technique, logic relaxation,[note 5] and a self-induced state/mode.<BR><BR>'
'Other criteria deemed important [but not essential] involve a state of psychophysical relaxation, the use of a self-focus skill or anchor, the presence of a state of suspension of logical thought processes, a religious/spiritual/philosophical context, or a state of mental silence.[23]: 135 .<BR><BR>'
'[...] It is plausible that meditation is best thought of as a natural category of techniques best captured by "family resemblances" [...] or by the related "prototype" model of concepts."[23]: 135 [note 6].<BR><BR>'
'Several other definitions of meditation have been used by influential modern reviews of research on meditation across multiple traditions:[note 7].<BR><BR>'
'Walsh & Shapiro (2006): "[M]editation refers to a family of self-regulation practices that focus on training attention and awareness in order to bring mental processes under greater voluntary control and thereby foster general mental well-being and development and/or specific capacities such as calm, clarity, and concentration"[1]: 228–29 .<BR>'
'Cahn & Polich (2006): "[M]editation is used to describe practices that self-regulate the body and mind, thereby affecting mental events by engaging a specific attentional set.... regulation of attention is the central commonality across the many divergent methods"[2]: 180 .<BR>'
'Jevning et al. (1992): "We define meditation... as a stylized mental technique... repetitively practiced for the purpose of attaining a subjective experience that is frequently described as very restful, silent, and of heightened alertness, often characterized as blissful"[3]: 415 .<BR>'
'Goleman (1988): "the need for the meditator to retrain his attention, whether through concentration or mindfulness, is the single invariant ingredient in... every meditation system"[4]: 107 '
'Separation of technique from tradition.<BR>''Meditation has proven difficult to define as it covers a wide range of dissimilar practices in different traditions. In popular usage, the word "meditation" and the phrase "meditative practice" are often used imprecisely to designate practices found across many cultures.[4][20] These can include almost anything that is claimed to train the attention of mind or to teach calm or compassion.[21] There remains no definition of necessary and sufficient criteria for meditation that has achieved universal or widespread acceptance within the modern scientific community. In 1971, Claudio Naranjo noted that "The word "meditation" has been used to designate a variety of practices that differ enough from one another so that we may find trouble in defining what meditation is."[22]: 6 A 2009 study noted a "persistent lack of consensus in the literature" and a "seeming intractability of defining meditation".[23]: 135 <BR><BR>'
'In modern psychological research, meditation has been defined and characterized in a variety of ways. Many of these emphasize the role of attention[4][1][2][3] and characterize the practice of meditation as attempts to get beyond the reflexive, "discursive thinking"[note 1] or "logic"[note 2] mind[note 3] to achieve a deeper, more devout, or more relaxed state.<BR><BR>'
'Bond et al. (2009) identified criteria for defining a practice as meditation "for use in a comprehensive systematic review of the therapeutic use of meditation", using "a 5-round Delphi study with a panel of 7 experts in meditation research" who were also trained in diverse but empirically highly studied (Eastern-derived or clinical) forms of meditation[note 4]:<BR><BR>'
'three main criteria [...] as essential to any meditation practice: the use of a defined technique, logic relaxation,[note 5] and a self-induced state/mode.<BR><BR>'
'Other criteria deemed important [but not essential] involve a state of psychophysical relaxation, the use of a self-focus skill or anchor, the presence of a state of suspension of logical thought processes, a religious/spiritual/philosophical context, or a state of mental silence.[23]: 135 .<BR><BR>'
'[...] It is plausible that meditation is best thought of as a natural category of techniques best captured by "family resemblances" [...] or by the related "prototype" model of concepts."[23]: 135 [note 6].<BR><BR>'
'Several other definitions of meditation have been used by influential modern reviews of research on meditation across multiple traditions:[note 7].<BR><BR>'
'Walsh & Shapiro (2006): "[M]editation refers to a family of self-regulation practices that focus on training attention and awareness in order to bring mental processes under greater voluntary control and thereby foster general mental well-being and development and/or specific capacities such as calm, clarity, and concentration"[1]: 228–29 .<BR>'
'Cahn & Polich (2006): "[M]editation is used to describe practices that self-regulate the body and mind, thereby affecting mental events by engaging a specific attentional set.... regulation of attention is the central commonality across the many divergent methods"[2]: 180 .<BR>'
'Jevning et al. (1992): "We define meditation... as a stylized mental technique... repetitively practiced for the purpose of attaining a subjective experience that is frequently described as very restful, silent, and of heightened alertness, often characterized as blissful"[3]: 415 .<BR>'
'Goleman (1988): "the need for the meditator to retrain his attention, whether through concentration or mindfulness, is the single invariant ingredient in... every meditation system"[4]: 107 '
'Separation of technique from tradition.<BR>'
'Some of the difficulty in precisely defining meditation has been in recognizing the particularities of the many various traditions;[27] and theories and practice can differ within a tradition.[28] Taylor noted that even within a faith such as "Hindu" or "Buddhist", schools and individual teachers may teach distinct types of meditation.[29]: 2 Ornstein noted that "Most techniques of meditation do not exist as solitary practices but are only artificially separable from an entire system of practice and belief."[30]: 143 For instance, while monks meditate as part of their everyday lives, they also engage the codified rules and live together in monasteries in specific cultural settings that go along with their meditative practices.<BR>')
main_container_layout.addWidget(description_editor)
main_container.setLayout(main_container_layout)
main_window.setCentralWidget(main_container)
def resizeEvent(self, event):
super().resizeEvent(event)
self.set_up_window()
def launch(self):
self.set_up_window()
self.showMaximized()
class TextEditor(QTextEdit):
def __init__(self):
super(TextEditor, self).__init__()
# make sure this widget is tracking the mouse position at all times
self.setMouseTracking(True)
self.last_mouse_position = None
@staticmethod
def get_selected_word(text_cursor):
# extract selected word
text_cursor.select(QTextCursor.WordUnderCursor)
word_under_cursor = text_cursor.selectedText()
# if the currently selected word is a '.', try to select the previous word
while word_under_cursor == '.':
# first move the character back one
text_cursor.movePosition(QTextCursor.PreviousCharacter)
# try to move to the beginning of the previous word
successfully_moved_to_previous_word = text_cursor.movePosition(QTextCursor.PreviousWord)
# if unsuccessful, try the next
if not successfully_moved_to_previous_word:
# undo the character move
text_cursor.movePosition(QTextCursor.NextCharacter, 1)
# try to move to the beginning of the next word
if text_cursor.movePosition(QTextCursor.NextWord):
text_cursor.select(QTextCursor.WordUnderCursor)
word_under_cursor = text_cursor.selectedText()
# move the QTextCursor from the '.' to the previous word
else:
text_cursor.select(QTextCursor.WordUnderCursor)
word_under_cursor = text_cursor.selectedText()
return word_under_cursor
def get_whole_sentence_containing_selected_word(self, selected_word, text=None):
if text is None:
as_text = self.toPlainText()
else:
as_text = text
# split into sentences
text_lines = as_text.split('.')
# add back the final stop
for index, line in enumerate(text_lines[:-1]):
text_lines[index] = line + '.'
sentences = list(chain(*[s.splitlines() for s in text_lines]))
# get the final selected sentence, if it's found
selected_sentence = [s for s in sentences if selected_word in s]
if len(selected_sentence) == 0:
selected_sentence = ''
else:
selected_sentence = selected_sentence[0]
return selected_sentence
def get_whole_paragraph_containing_selected_word(self, selected_word):
as_text = self.toPlainText()
text_paragraphs = as_text.split('\n')
selected_paragraph = [p for p in text_paragraphs if selected_word in p]
return selected_paragraph[0] if len(selected_paragraph) > 0 else ''
def replace_in_html(self, old_string, new_string, case_insensitive=False):
if not case_insensitive:
old_html = self.toHtml()
new_html = old_html.replace(old_string, new_string)
self.setHtml(new_html)
else:
old_html = self.toHtml()
pattern = re.compile(old_string, re.IGNORECASE)
new_html = pattern.sub(new_string, old_html)
self.setHtml(new_html)
def setText(self, text: str) -> None:
extra_newlines_removed = text.replace('<BR><BR><BR>', '<BR><BR>').replace('\n\n', '\n')
super().setText(extra_newlines_removed)
def setHtml(self, text: str) -> None:
old_value = self.verticalScrollBar().sliderPosition()
old_value_hor = self.horizontalScrollBar().sliderPosition()
super().setHtml(text)
self.verticalScrollBar().setSliderPosition(old_value)
self.horizontalScrollBar().setSliderPosition(old_value_hor)
def mouse_inside_editor(self, position):
self.last_mouse_position = position
# create a QTextCursor at that position to select text
text_cursor = self.cursorForPosition(position)
# get the currently selected word
word_under_cursor = self.get_selected_word(text_cursor)
# replace substring with placeholder containing as many characters
selected_word_placeholder = self.replace_selected_text_with_placeholder(text_cursor, 'ª')
selected_fragment = self.get_whole_sentence_containing_selected_word(selected_word_placeholder)
word_under_cursor = '<span style="background-color: #FFFF00;text-decoration:underline;">' + word_under_cursor + '</span>'
highlighted_text = selected_fragment.replace(selected_word_placeholder, word_under_cursor)
# replace the sentence with the new formatting
self.replace_in_text(selected_fragment, highlighted_text)
@staticmethod
def replace_selected_text_with_placeholder(text_cursor, character):
# remove the selected word to be replaced by the placeholder
text_cursor.removeSelectedText()
# create a placeholder with as many characters as the original word
word_placeholder = ''
for char in range(10):
word_placeholder += character
text_cursor.insertText(word_placeholder)
return word_placeholder
def mouseMoveEvent(self, mouse_event: QMouseEvent) -> None:
if self.underMouse():
self.mouse_inside_editor(mouse_event.pos())
def replace_in_text(self, text_to_replace, text_to_replace_with):
# replace newlines with a placeholder so they are kept once the HTML is removed to remove the previous formatting
self.replace_in_html('<br/>', 'PL_BR', True)
self.replace_in_html('<br />', 'PL_BR', True)
# replace in the text
replaced_text = self.toPlainText().replace(text_to_replace, text_to_replace_with)
# remove stray final stops
replaced_text = replaced_text.replace('..', '.')
replaced_text = replaced_text.replace('PL_BR. ', 'PL_BR')
# remove excessive empty lines
replaced_text = replaced_text.replace('PL_BRPL_BRPL_BR', 'PL_BRPL_BR')
while replaced_text.startswith('PL_BR'):
replaced_text = replaced_text[5:]
# return new lines
final_html = replaced_text.replace('PL_BR', '<br/>')
self.setHtml(final_html)
# last stray final stops
self.replace_in_html('">.<br/>', '"><br/>')
main_window = GUIMainWindow()
main_window.launch()
application.exec()
Works for this dimension:
Only works for the portion shown in red. Anything below that is just taken to this same position:
Solution
Note: this answer does not directly reply to the question, since the origin of the problem was not related to the scrollbars
The problem is caused by the fact that setHtml
always resets the contents. While the result might appear instantaneous, it is not, since the document has to be layed out again based on the size of the scroll area, which eventually updates the scroll bars.
While technically it is possible to delay the scrollbar reposition, the real issue is that setHTML
is not the proper way to provide highlighting, especially for mouse hovering: every single individual movement of the mouse would cause what explained above, and this is clearly not a good solution, performance wise.
It's also important to remember that setting the HTML in a Qt editor is only interpreted, since the HTML is parsed and then "translated" into a Qt document; you'll see that toHtml()
will not return the code used for setHtml()
, even if no change has been made.
In fact, the proper way to alter the format of a Qt rich text editor, is to interact with its lower level API, accessing the QTextDocument and modifying it using a QTextCharFormat.
Note that while normal editing is applied using the QTextCursor interface, it would not be effective for this purpose, since it is considered as an "user action" and adds the format change to the undo stack of the document.
In order to apply changes to the underlying document without changing its contents, it's necessary to use the QTextLayout of the QTextBlock that will contain the highlighted text, then it's possible to create and set a FormatRange for that layout by specifying the character boundaries and actual format.
This approach has the benefit that the format changes do not change the actual document formats, but only the way the document is displayed, so, for instance, toHtml()
will never include the highlighting.
This is the summarized procedure:
- get the QTextCursor based on the mouse cursor position;
- select the word under the mouse;
- get the QTextBlock and ensure that the mouse is actually inside the rectangle of that block (
cursor.WordUnderCursor
always finds the closest word, even if it's not exactly under the mouse); - get the range of the selection and check if the range is changed from the previous call;
- clear the previous layout format (it could be a different word or even a different block);
- if there is a selection, create a FormatRange with the start position based on the block cursor position, and with a given text format;
- set the format on the layout;
- notify the document that its contents are "dirty" and need to be layed out again (and repainted);
In order to call the function whenever the mouse moves, the mouseTracking
property should be set, and the viewportEvent
must be overridden to properly track mouse events.
from PyQt5 import QtCore, QtGui, QtWidgets
from random import randrange, choice
from string import ascii_lowercase as letters
class HighlightTextEdit(QtWidgets.QTextEdit):
highlightPos = -1, -1
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setMouseTracking(True)
self.highlightBlock = self.document().firstBlock()
self.highlightFormat = QtGui.QTextCharFormat()
self.highlightFormat.setBackground(
QtGui.QBrush(QtGui.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(QtGui.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 = QtGui.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(QtCore.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)
if __name__ == '__main__':
text = ''
for p in range(randrange(10, 30)):
parag = []
for w in range(randrange(5, 50)):
word = ''
for l in range(randrange(2, 20)):
word += choice(letters)
if not randrange(10):
word += ','
parag.append(word)
text += ' '.join(parag).capitalize().rstrip(',') + '.\n\n'
import sys
app = QtWidgets.QApplication(sys.argv)
test = HighlightTextEdit()
test.setText(text)
test.show()
sys.exit(app.exec_())
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.