2

This follows on directly from this question. Here is an MRE:

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Get a grip of table view row height MRE')
        self.setGeometry(QtCore.QRect(100, 100, 1000, 800))
        layout = QtWidgets.QVBoxLayout()
        central_widget = QtWidgets.QWidget( self )
        central_widget.setLayout(layout)
        self.table_view = SegmentsTableView(self)
        self.setCentralWidget(central_widget)
        layout.addWidget(self.table_view)
        rows = [
         ['one potatoe two potatoe', 'one potatoe two potatoe'],
         ['Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque',
          'Sed ut <b>perspiciatis, unde omnis <i>iste natus</b> error sit voluptatem</i> accusantium doloremque'],
         ['Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.',
          'Nemo enim ipsam <i>voluptatem, quia voluptas sit, <b>aspernatur aut odit aut fugit, <u>sed quia</i> consequuntur</u> magni dolores eos</b>, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.'
          ],
         ['Ut enim ad minima veniam',
          'Ut enim ad minima veniam'],
         ['Quis autem vel eum iure reprehenderit',
          'Quis autem vel eum iure reprehenderit'],
         ['At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.',
          'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'
         ]]
        for n_row, row in enumerate(rows):
            self.table_view.model().insertRow(n_row)
            self.table_view.model().setItem(n_row, 0, QtGui.QStandardItem(row[0]))
            self.table_view.model().setItem(n_row, 1, QtGui.QStandardItem(row[1]))
        self.table_view.setColumnWidth(0, 400)
        self.table_view.setColumnWidth(1, 400)
        self.qle = QtWidgets.QLineEdit()
        layout.addWidget(self.qle)
        self._second_timer = QtCore.QTimer(self)
        self._second_timer.timeout.connect(self.show_doc_size)
        # every 1s 
        self._second_timer.start(1000)
        
    def show_doc_size(self, *args):
        if self.table_view.itemDelegate().editor == None:
            self.qle.setText('no editor yet')
        else:
            self.qle.setText(f'self.table_view.itemDelegate().editor.document().size() {self.table_view.itemDelegate().editor.document().size()}')
        
class SegmentsTableView(QtWidgets.QTableView):
    def __init__(self, parent):
        super().__init__(parent)
        self.setItemDelegate(SegmentsTableViewDelegate(self))
        self.setModel(QtGui.QStandardItemModel())
        v_header =  self.verticalHeader()
        # 
        v_header.setMinimumSectionSize(5)
        v_header.sectionHandleDoubleClicked.disconnect()
        v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
        self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)        
        
    def resizeRowToContents(self, row):
        print(f'row {row}')
        super().resizeRowToContents(row)
        self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))

    def resizeRowsToContents(self):
        header = self.verticalHeader()
        for row in range(self.model().rowCount()):
            hint = self.sizeHintForRow(row)
            header.resizeSection(row, hint)    
            
    def sizeHintForRow(self, row):
        super_result = super().sizeHintForRow(row)
        print(f'row {row} super_result {super_result}')
        return super_result
        
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, *args):
        super().__init__(*args)
        self.editor = None
    
    def createEditor(self, parent, option, index):
        class Editor(QtWidgets.QTextEdit):
            def resizeEvent(self, event):
                print(f'event {event}')
                super().resizeEvent(event)
        self.editor = Editor(parent)
        # does not seem to solve things:
        self.editor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

        class Document(QtGui.QTextDocument):
            def __init__(self, *args):
                super().__init__(*args)
                self.contentsChange.connect(self.contents_change)
            
            def drawContents(self, p, rect):
                print(f'p {p} rect {rect}')
                super().drawContents(p, rect)
                
            def contents_change(self, position, chars_removed, chars_added):
                # strangely, after a line break, this shows a higher rect NOT when the first character 
                # causes a line break... but after that!
                print(f'contents change, size {self.size()}')
                # parent.parent() is the table view
                parent.parent().resizeRowToContents(index.row())
                
        self.editor.setDocument(Document())
        return self.editor
    
    def paint(self, painter, option, index):
        doc = QtGui.QTextDocument()
        doc.setDocumentMargin(0)
        doc.setDefaultFont(option.font)
        self.initStyleOption(option, index)
        painter.save()
        doc.setTextWidth(option.rect.width())
        doc.setHtml(option.text)
        option.text = ""
        option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
        painter.translate(option.rect.left(), option.rect.top())
        clip = QtCore.QRectF(0, 0, option.rect.width(), option.rect.height())
        painter.setClipRect(clip)
        ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
        ctx.clip = clip
        doc.documentLayout().draw(painter, ctx)
        painter.restore()
            
    def sizeHint(self, option, index):
        self.initStyleOption(option, index)
        doc = QtGui.QTextDocument()
        if self.editor != None and index.row() == 0:
            print(f'self.editor.size() {self.editor.size()}')
            print(f'self.editor.document().size() {self.editor.document().size()}')
        doc.setTextWidth(option.rect.width())
        doc.setDefaultFont(option.font)
        doc.setDocumentMargin(0)
        doc.setHtml(option.text)
        doc_height_int = int(doc.size().height())
        if self.editor != None and index.row() == 0:
            print(f'...row 0 doc_height_int {doc_height_int}')
        return QtCore.QSize(int(doc.idealWidth()), doc_height_int)
                
app = QtWidgets.QApplication([])
app.setFont(default_font)
main_window = MainWindow()
main_window.show()
exec_return = app.exec()
sys.exit(exec_return)   

If I start editing the right-hand cell in row 0 (F2 or double-click), and start typing, slowly at the end of the existing text, the words "four five six seven", I find that a line break (word-wrap) occurs when I type the "n" of seven. But the line which at that moment prints f'contents change, size {self.size()}' shows that the document height is still only 32.0. It is only when I type another character that this increases to 56.0.

I want the row to expand in height as the editor (or its document?) grows in height.

There are a couple of other puzzles: when I type in this editor, the characters are currently jumping up and down a bit. Secondly, the line self.editor.document().size() (in sizeHint) is printed 4 times when I type each character. To me both phenomena suggest that I might be short-circuiting signals in some way, or in some way doing things in the wrong way.

As described, I have not been able to find any way of measuring the true height of the QTextDocument (or its QTextEdit editor) immediately after a line break, or indeed anything like a signal which is emitted when a line break occurs (in this connection I also looked at QTextCursor, for example).

Edit

I've now changed the main window constructor a bit so a QLE can show the dimensions of the QTextDocument in deferred fashion (NB can't use a button because clicking takes focus away and destroys the editor). So please try new version as above if intrigued.

What this shows is rather revealing: you will see that the next 1-second "tick" after the word-wrap occurs, the correct height for the document is given in the QLE. This suggests to me that there is some sort of deferred triggering going on here. But because I haven't been able to find a suitable method or signal which activates when the QTextDocument changes size, I'm not sure how it is possible to respond to that.

PS it works the other way too: if you slowly delete characters after having provoked a word-wrap, until the text becomes one line again, the QLE shows the right height of 32.0 while contents_change continues to show an incorrect height of 56.0.

mike rodent
  • 14,126
  • 11
  • 103
  • 157

2 Answers2

3

Resizing the row based on the contents might present some problems, and it might cause recursion you're not very careful, or at least calling a lot of functions unnecessarily, for many, many times.

The default behavior of Qt delegates is to adapt the string editor (a QLineEdit subclass) based on the contents, making it eventually larger (and never smaller) than its original cell, in order to show as much content as possible, but not larger than the right margin of the view.
While this behavior works fine, implementing it for a multi-line editor becomes much more complex: a vertical scroll bar should be probably shown (but that creates some problems due to recursion of the document size based on their visibility), and borders around the editor should be visible in order to understand when the actual content ends (otherwise you might mistake the content of the next row for the content of the editor); considering that, resizing the row might make sense, but, as said, careful precautions might be taken. A well written implementation should consider that and possibly inherit the same behavior by properly resizing the editor.

That said, here are some suggestions:

  • there is usually no real benefit in creating classes inside a function; if you want to make a class "private", just use the double underscore prefix for its name; if you do it in order to access local variables, then it probably means that the whole logic is conceptually wrong: a class should (theoretically) not care about the local scope in which it was created, but only the global environment;
  • changes in the content of a QTextEdit's document require event loop processing in order to be effective: the scroll area needs to update its contents based on the layout system and then effectively resize the document layout; you must use a 0-timeout timer in order to get the actual new size of the document;
  • while in your case only one editor will theoretically exist at any given time, the delegate should not keep a unique static reference for the editor: you might want to use openPersistentEditor() at some point, and that will break a lot of things; unfortunately Qt doesn't provide a public API for the current open editor at a given index, but you can create a dictionary for that; the catch is that you should use a QPersistentModelIndex to be perfectly safe (this is very important if the model supports sorting/filtering or it could be updated externally by another function or thread);
  • the toHtml() function automatically sets the p, li { white-space: pre-wrap; } stylesheet (it's hardcoded, so it cannot be overridden, search for QTextHtmlExporter::toHtml in the sources); since the first paragraph will always begin with a new line for the <p> tag, this means that the resulting QTextDocument based on that HTML will have a pre-wrap new line using the paragraph line spacing. Since item delegates use the editor's user property (which is the html property for QTextEdit) to set the editor data and then submit it to the model, the solution is to create a custom Qt property (with the user flag set to True that would override the existing one) and return the result of toHtml() without the first line break after the <body> tag;
  • clicking outside the index to commit the data is unintuitive; you can override the delegate eventFilter function to capture a keyboard shortcut, like Ctrl+Return;
  • using the document layout with a paint context is normally unnecessary in these situations, and you can just translate the painter and use drawContents;

Considering the above, here's a possible solution:

class DelegateRichTextEditor(QtWidgets.QTextEdit):
    commit = QtCore.pyqtSignal(QtWidgets.QWidget)
    sizeHintChanged = QtCore.pyqtSignal()
    storedSize = None

    def __init__(self, parent):
        super().__init__(parent)
        self.setFrameShape(0)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.contentTimer = QtCore.QTimer(self, 
            timeout=self.contentsChange, interval=0)
        self.document().setDocumentMargin(0)
        self.document().contentsChange.connect(self.contentTimer.start)

    @QtCore.pyqtProperty(str, user=True)
    def content(self):
        text = self.toHtml()
        # find the end of the <body> tag and remove the new line character
        bodyTag = text.find('>', text.find('<body')) + 1
        if text[bodyTag] == '\n':
            text = text[:bodyTag] + text[bodyTag + 1:]
        return text

    @content.setter
    def content(self, text):
        self.setHtml(text)

    def contentsChange(self):
        newSize = self.document().size()
        if self.storedSize != newSize:
            self.storedSize = newSize
            self.sizeHintChanged.emit()

    def keyPressEvent(self, event):
        if event.modifiers() == QtCore.Qt.ControlModifier:
            if event.key() in (QtCore.Qt.Key_Return, ):
                self.commit.emit(self)
                return
            elif event.key() == QtCore.Qt.Key_B:
                if self.fontWeight() == QtGui.QFont.Bold:
                    self.setFontWeight(QtGui.QFont.Normal)
                else:
                    self.setFontWeight(QtGui.QFont.Bold)
            elif event.key() == QtCore.Qt.Key_I:
                self.setFontItalic(not self.fontItalic())
            elif event.key() == QtCore.Qt.Key_U:
                self.setFontUnderline(not self.fontUnderline())
        super().keyPressEvent(event)

    def showEvent(self, event):
        super().showEvent(event)
        cursor = self.textCursor()
        cursor.movePosition(cursor.End)
        self.setTextCursor(cursor)


class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
    rowSizeHintChanged = QtCore.pyqtSignal(int)
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.editors = {}

    def createEditor(self, parent, option, index):
        pIndex = QtCore.QPersistentModelIndex(index)
        editor = self.editors.get(pIndex)
        if not editor:
            editor = DelegateRichTextEditor(parent)
            editor.sizeHintChanged.connect(
                lambda: self.rowSizeHintChanged.emit(pIndex.row()))
            self.editors[pIndex] = editor
        return editor

    def eventFilter(self, editor, event):
        if (event.type() == event.KeyPress and 
            event.modifiers() == QtCore.Qt.ControlModifier and 
            event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return)):
                self.commitData.emit(editor)
                self.closeEditor.emit(editor)
                return True
        return super().eventFilter(editor, event)

    def destroyEditor(self, editor, index):
        # remove the editor from the dict so that it gets properly destroyed;
        # this avoids any "wrapped C++ object destroyed" exception
        self.editors.pop(QtCore.QPersistentModelIndex(index))
        super().destroyEditor(editor, index)
        # emit the signal again: if the data has been rejected, we need to
        # restore the correct hint
        self.rowSizeHintChanged.emit(index.row())


    def paint(self, painter, option, index):
        self.initStyleOption(option, index)
        painter.save()
        doc = QtGui.QTextDocument()
        doc.setDocumentMargin(0)
        doc.setTextWidth(option.rect.width())
        doc.setHtml(option.text)
        option.text = ""
        option.widget.style().drawControl(
            QtWidgets.QStyle.CE_ItemViewItem, option, painter)
        painter.translate(option.rect.left(), option.rect.top())
        doc.drawContents(painter)
        painter.restore()

    def sizeHint(self, option, index):
        self.initStyleOption(option, index)
        editor = self.editors.get(QtCore.QPersistentModelIndex(index))
        if editor:
            doc = QtGui.QTextDocument.clone(editor.document())
        else:
            doc = QtGui.QTextDocument()
            doc.setDocumentMargin(0)
            doc.setHtml(option.text)
            doc.setTextWidth(option.rect.width())
        doc_height_int = int(doc.size().height())
        return QtCore.QSize(int(doc.idealWidth()), doc_height_int)


class SegmentsTableView(QtWidgets.QTableView):
    def __init__(self, parent):
        super().__init__(parent)
        delegate = SegmentsTableViewDelegate(self)
        self.setItemDelegate(delegate)
        delegate.rowSizeHintChanged.connect(self.resizeRowToContents)
        # ...

Further notes:

  • while the paint function is usually called last in the event loop, some care should be taken when overriding the option values; when the "modified" option is only used temporarily (for instance, querying the current style with changed values), it's good habit to create a new option based on the given one; you can use a new option by doing the following:
    newOption = option.__class__(option)
  • "Secondly, the line self.editor.document().size() (in sizeHint) is printed 4 times when I type each character": this is because resizeRowToContents is being triggered by contents_change; that resize function automatically calls sizeHint() of the delegate for all indexes in the given row to get all available hints, then it resizes all the sections according to the width computation, but since resizeRowsToContents is connected to resizeRowsToContents it will be called again if the row sizes don't match; this is a typical example for which one must be very careful in changing geometries after some "resize" event, since they could cause some (possibly, infinite) level of recursion;
  • the only drawback of this is that keyboard repetition is not taken into account, so the editor (and the view) won't be updated until a repeated key is actually released; this could be solved by using an alternate timer that triggers the contentsChange of the editor whenever a isAutoRepeat() key event is captured;
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks, this is great. Over the past hour I've been making my own full MRE version based on your comments of yesterday, including something which sanitises the HTML delivered by the `QTextEdit`. You might want to take a look at it (in my answer), meanwhile I'll take a look at this solution and see if there's more for me to improve. – mike rodent Sep 10 '21 at 07:23
1

After a bit of trial and error, I found that crucially there is a signal which is triggered when the QTextDocument changes height, cursorPositionChanged. So you make the latter trigger parent.parent().resizeRowToContents(index.row()). There may be scope for a check at this point to see whether the document has actually changed height (not all cursor changes imply that of course).

Then you can do this in sizeHint:

if self.editor == None:
    doc_height_int = int(doc.size().height())
else:     
    doc_height_int = int(self.editor.document().size().height())

You then need to self.setDocumentMargin(0) in the Document constructor and also add this method to the delegate (if the editor is destroyed but not None you get one of those horrible "wrapped C++ object destroyed" Runtime Errors):

def destroyEditor(self, editor, index):
    super().destroyEditor(editor, index)
    self.editor = None

There's still a bit of wobble when writing in a single-line text... this is probably due to competing impulses somewhere saying what the size hint of something is.

Edit

Based on Musicamante's comments, here is my new, full version of the MRE. You have to pip install the module dominate (creates HTML entities).

NB I haven't tackled the way of ending an edit session: it is possible (at least on W10 OS) to end an edit by pressing Ctrl-Down for example.

import sys, html.parser, dominate 
from PyQt5 import QtWidgets, QtCore, QtGui

class MainWindow(QtWidgets.QMainWindow):
        def __init__(self):
            super().__init__()
            self.setWindowTitle('Implement HTML editor for QTableView')
            self.setGeometry(QtCore.QRect(100, 100, 1000, 800))
            self.table_view = SegmentsTableView(self)
            self.setCentralWidget(self.table_view)
            rows = [
             ['one potatoe two potatoe', 'one potatoe two potatoe'],
             ['Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque',
              'Sed ut <b>perspiciatis, unde omnis <i>iste natus</b> error sit voluptatem</i> accusantium doloremque'],
             ['Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.',
              'Nemo enim ipsam <i>voluptatem, quia voluptas sit, <b>aspernatur aut odit aut fugit, <u>sed quia</i> consequuntur</u> magni dolores eos</b>, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui do lorem ipsum, quia dolor sit amet consectetur adipiscing velit, sed quia non numquam do eius modi tempora incididunt, ut labore et dolore magnam aliquam quaerat voluptatem.'
              ],
             ['Ut enim ad minima veniam',
              'Ut enim ad minima veniam'],
             ['Quis autem vel eum iure reprehenderit',
              'Quis autem vel eum iure reprehenderit'],
             ['At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.',
              'At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum deleniti atque corrupti, quos dolores et quas molestias excepturi sint, obcaecati cupiditate non provident, similique sunt in culpa, qui officia deserunt mollitia animi, id est laborum et dolorum fuga.'
             ]]
            for n_row, row in enumerate(rows):
                self.table_view.model().insertRow(n_row)
                self.table_view.model().setItem(n_row, 0, QtGui.QStandardItem(row[0]))
                self.table_view.model().setItem(n_row, 1, QtGui.QStandardItem(row[1]))
            self.table_view.setColumnWidth(0, 400)
            self.table_view.setColumnWidth(1, 400)
        
class SegmentsTableView(QtWidgets.QTableView):
    def __init__(self, parent):
        super().__init__(parent)
        self.setItemDelegate(SegmentsTableViewDelegate(self))
        self.setModel(QtGui.QStandardItemModel())
        v_header =  self.verticalHeader()
        # 
        v_header.setMinimumSectionSize(5)
        v_header.sectionHandleDoubleClicked.disconnect()
        v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
        self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)        
        
    def resizeRowToContents(self, row):
        super().resizeRowToContents(row)
        self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))

    def resizeRowsToContents(self):
        header = self.verticalHeader()
        for row in range(self.model().rowCount()):
            hint = self.sizeHintForRow(row)
            header.resizeSection(row, hint)    
            
class SegmentsTableViewDelegate(QtWidgets.QStyledItemDelegate):
    class EditorDocument(QtGui.QTextDocument):
        def __init__(self, parent):
            super().__init__(parent)
            self.setDocumentMargin(0)
            self.contentsChange.connect(self.contents_change)
            self.height = None
            parent.setDocument(self)
        
        def contents_change(self, position, chars_removed, chars_added):
            def resize_func():
                if self.size().height() != self.height:
                    doc_size = self.size()
                    self.parent().resize(int(doc_size.width()), int(doc_size.height()))
            QtCore.QTimer.singleShot(0, resize_func)
            
    class EditorHTMLSanitiserParser(html.parser.HTMLParser):
        def feed(self, html_string):
            self.reset()
            self.started_constructing = False
            self.sanitised_html_string = ''
            super().feed(html_string)
    
        def handle_starttag(self, tag, attrs):
            # I believe you can't insert a P into your displayed HTML (Shift-Return inserts a BR)
            if tag == 'p':
                self.started_constructing = True
            elif self.started_constructing:
                if tag in dir(dominate.tags):
                    new_tag_entity = getattr(dominate.tags, tag)()
                    for attr_name, attr_val in attrs:
                        new_tag_entity[attr_name] = attr_val
                    # remove the closing tag characters from the end of the rendered HTML    
                    self.sanitised_html_string += new_tag_entity.render()[:-(len(tag) + 3)]
                else:
                    print(f'*** unrecognised HTML tag: |{tag}|')

        def handle_endtag(self, tag):
            if self.started_constructing:
                if tag in dir(dominate.tags):
                    new_tag_entity = getattr(dominate.tags, tag)()
                    # append only the closing tag characters from the end of the rendered HTML    
                    self.sanitised_html_string += new_tag_entity.render()[-(len(tag) + 3):]
                else:
                    print(f'*** unrecognised HTML tag: |{tag}|')
                
        def handle_data(self, data):
            if self.started_constructing:
                self.sanitised_html_string += html.escape(data)
    
    def __init__(self, *args):
        super().__init__(*args)
        self.pm_index_to_editor_dict = {}
        self.html_sanitiser = SegmentsTableViewDelegate.EditorHTMLSanitiserParser()
    
    def createEditor(self, parent, option, index):
        class Editor(QtWidgets.QTextEdit):
            def resizeEvent(self, event):
                super().resizeEvent(event)
                parent.parent().resizeRowToContents(index.row())

            # taken from Musicamante: apply bold/italic/underline
            def keyPressEvent(self, event):
                if event.modifiers() == QtCore.Qt.ControlModifier:
                    if event.key() in (QtCore.Qt.Key_Return, ):
                        self.commit.emit(self)
                        return
                    elif event.key() == QtCore.Qt.Key_B:
                        if self.fontWeight() == QtGui.QFont.Bold:
                            self.setFontWeight(QtGui.QFont.Normal)
                        else:
                            self.setFontWeight(QtGui.QFont.Bold)
                    elif event.key() == QtCore.Qt.Key_I:
                        self.setFontItalic(not self.fontItalic())
                    elif event.key() == QtCore.Qt.Key_U:
                        self.setFontUnderline(not self.fontUnderline())
                super().keyPressEvent(event)                


        pm_index = QtCore.QPersistentModelIndex(index)
        if pm_index in self.pm_index_to_editor_dict:
            editor = self.pm_index_to_editor_dict[pm_index]
        else:
            editor = Editor(parent)
            editor.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
            editor.setFrameShape(0)
            self.pm_index_to_editor_dict[pm_index] = editor                    
        SegmentsTableViewDelegate.EditorDocument(editor)
        return editor
    
    def destroyEditor(self, editor, index):
        super().destroyEditor(editor, index)
        pm_index = QtCore.QPersistentModelIndex(index)
        del self.pm_index_to_editor_dict[pm_index]
        # from Musicamante: case of rejected edit
        self.parent().resizeRowToContents(index.row())
        
    def setModelData(self, editor, model, index):
        self.html_sanitiser.feed(editor.toHtml())
        model.setData(index, self.html_sanitiser.sanitised_html_string, QtCore.Qt.EditRole)
    
    def paint(self, painter, option, index):
        doc = QtGui.QTextDocument()
        doc.setDocumentMargin(0)
        doc.setDefaultFont(option.font)
        self.initStyleOption(option, index)
        painter.save()
        doc.setTextWidth(option.rect.width())
        doc.setHtml(option.text)
        option.text = ""
        option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter)
        painter.translate(option.rect.left(), option.rect.top())
        # from Musicamante
        doc.drawContents(painter)
        painter.restore()
            
    def sizeHint(self, option, index):
        self.initStyleOption(option, index)
        pm_index = QtCore.QPersistentModelIndex(index)
        if pm_index in self.pm_index_to_editor_dict:
            doc = self.pm_index_to_editor_dict[pm_index].document()
        else:
            doc = QtGui.QTextDocument()
            doc.setTextWidth(option.rect.width())
            doc.setDefaultFont(option.font)
            doc.setDocumentMargin(0)
            doc.setHtml(option.text)
        return QtCore.QSize(int(doc.idealWidth()), int(doc.size().height()))
                
app = QtWidgets.QApplication([])
app.setFont(default_font)
main_window = MainWindow()
main_window.show()
exec_return = app.exec()
sys.exit(exec_return)    
mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • I see some problems with your implementation, including the fact that you're using a static and unique reference to the editor: while conceptually might work in your case, technically there could be more than one active editor at the same time (when using `openPersistentEditor`, for instance), so you should better use a dictionary with QPersistentModelIndex as keys and the editor as values, and then eventually remove the key when the editor is destroyed. That said, I don't think it's a good idea to resize the row whenever the content change. What you could do instead is to resize the editor. – musicamante Sep 09 '21 at 18:17
  • Thanks for those suggestions... I'll look into them. – mike rodent Sep 09 '21 at 18:19
  • This is also the same default behavior for QLineEdit in Qt item views: when the contents is larger than the available cell, it's the editor that is resized, and the cell is then resized to contents (if the header requires it) when the data is finally committed to the model. This also is better for performance and for code logic, as it makes it all simpler. Note that instead of using the cursor change (which can be potentially called lots of times even when the content is not changed), you could just call the resize in `contents_change` with a `QTimer.singleShot(0, resizeFunc)`. – musicamante Sep 09 '21 at 18:19
  • Aha... also makes sense. Thanks. – mike rodent Sep 09 '21 at 18:21
  • Finally, there are 3 problems that you didn't consider yet: 1. the editor has a focus border, which is very annoying (especially for one line content), maybe you don't see it in your system, but on certain styles it partially covers the text, so you should use `editor.setFrameShape(0)`, which is equivalent to the `setFrame(False)` used on QLineEdit for the default string editor. 2. the text set on the model is the HTML of the QTextEdit, which is formatted and has lots of content margins that break the original content, and the result is that the new drawn text is bigger than required. – musicamante Sep 09 '21 at 18:23
  • 3. The editor has no way to get "submitted", so you should probably manage the `keyPressEvent` (for instance, using ctrl+return), and consider [`tabChangesFocus`](https://doc.qt.io/qt-5/qtextedit.html#tabChangesFocus-prop). – musicamante Sep 09 '21 at 18:25
  • Will look into those too. In my real app I have to "sanitise" some of the HTML produced by the `QTextDocument`, so this might solve that. By the way, with my code as shown, on your system, do you see lots of wobbling up and down as you type when the text is only one line? As I said, I suspect this is the same issue you explained with my previous question. – mike rodent Sep 09 '21 at 18:27
  • The up/down is due to the margins of the editor as said before: when the cursor moves, the editor tries to make the cursor visible by scrolling the contents, and with such small values the result is the erratic movement. Other than using `setFrameShape(0)`, you should set to 0 the margins of `Document()` too. – musicamante Sep 09 '21 at 18:51
  • This code gives me `AttributeError: 'Editor' object has no attribute 'commit'` when I type control-enter (comes from line 125) – backseat Aug 31 '22 at 17:21