Search code examples
pythonpyqtpyqt6

Detect and handle Escape key editing QTableView cell


My application has a subclassed QTableWidget. In use, I've lost a few edits through hitting the Escape key (vim habit), so I'm trying to intercept the Escape key to ask whether or not to abandon the edit.

I also use control-enter to close and save the edited cell.

I have the code below as a proof of concept. It almost works. The Escape key is detected and handled as I want.

However, I'm doing something wrong. After either saving a modified cell with control-enter or abandoning it with Escape, I can no longer highlight the cell with a mouse click. Instead, a cursor appears as if the cell is still being edited.

Any pointers to fixing this would be appreciated, thanks.

#!/usr/bin/env python3

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QMainWindow,
    QMessageBox,
    QPlainTextEdit,
    QStyledItemDelegate,
    QStyleOptionViewItem,
    QTableWidget,
    QTableWidgetItem,
)


class EscapeLineEdit(QPlainTextEdit):
    """
    Subclass QPlainTextEdit to detect Escape key.
    """

    def __init__(self, parent, delegate):
        super().__init__(parent)
        self.delegate = delegate

    def keyPressEvent(self, event):
        if event.key() == Qt.Key.Key_Escape:
            discard_edits = QMessageBox.question(
                self.parent(), "Abandon edit", "Discard changes?")
            if discard_edits == QMessageBox.StandardButton.Yes:
                self.close()
        elif event.key() == Qt.Key.Key_Return:
            if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
                self.delegate.commitData.emit(self)
                self.delegate.closeEditor.emit(self)
                self.close()
        super().keyPressEvent(event)


class EscapeDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        return EscapeLineEdit(parent, self)


class MyTableWidget(QTableWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setItemDelegate(EscapeDelegate())
        self.cellDoubleClicked.connect(self.editCell)

    def editCell(self, row, col):
        """
        Explicitly set the cell editor to enable
        detection of the Escape key
        """

        delegate = self.itemDelegate()
        index = self.model().index(row, col)
        editor = delegate.createEditor(self.viewport(), QStyleOptionViewItem(), index)
        delegate.setEditorData(editor, index)
        self.setCellWidget(row, col, editor)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.table_widget = MyTableWidget(self)
        self.table_widget.setRowCount(2)
        self.table_widget.setColumnCount(2)
        for row in range(2):
            for col in range(2):
                item = QTableWidgetItem()
                item.setText(f"Row {row}, Col {col}")
                self.table_widget.setItem(row, col, item)
        self.setCentralWidget(self.table_widget)

        self.table_widget.resizeColumnsToContents()
        self.table_widget.resizeRowsToContents()


if __name__ == '__main__':
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

Solution

  • Ah, the Stackoverflow Rubber Duck works again... In case someone else is searching for something similar, this is how I fixed it (and it's much simpler):

    #!/usr/bin/env python3
    
    from PyQt6.QtCore import Qt, QEvent, QObject
    from PyQt6.QtWidgets import (
        QAbstractItemDelegate,
        QAbstractItemView,
        QApplication,
        QMainWindow,
        QMessageBox,
        QPlainTextEdit,
        QStyledItemDelegate,
        QStyleOptionViewItem,
        QTableWidget,
        QTableWidgetItem,
    )
    
    from PyQt6.QtGui import QKeyEvent
    
    from typing import cast
    
    
    class EscapeDelegate(QStyledItemDelegate):
        def __init__(self, parent=None):
            super().__init__(parent)
    
        def createEditor(self, parent, option, index):
            return QPlainTextEdit(parent)
    
        def eventFilter(self, editor: QObject, event: QEvent):
            """By default, QPlainTextEdit doesn't handle enter or return"""
    
            print("EscapeDelegate event handler")
            if event.type() == QEvent.Type.KeyPress:
                key_event = cast(QKeyEvent, event)
                if key_event.key() == Qt.Key.Key_Return:
                    if key_event.modifiers() == (
                        Qt.KeyboardModifier.ControlModifier
                    ):
                        print("save data")
                        self.commitData.emit(editor)
                        self.closeEditor.emit(editor)
                        return True
                elif key_event.key() == Qt.Key.Key_Escape:
                    discard_edits = QMessageBox.question(
                        self.parent(), "Abandon edit", "Discard changes?")
                    if discard_edits == QMessageBox.StandardButton.Yes:
                        print("abandon edit")
                        self.closeEditor.emit(editor)
                        return True
            return False
    
    
    class MyTableWidget(QTableWidget):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setItemDelegate(EscapeDelegate(self))
            self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
    
    
    class MainWindow(QMainWindow):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.table_widget = MyTableWidget(self)
            self.table_widget.setRowCount(2)
            self.table_widget.setColumnCount(2)
            for row in range(2):
                for col in range(2):
                    item = QTableWidgetItem()
                    item.setText(f"Row {row}, Col {col}")
                    self.table_widget.setItem(row, col, item)
            self.setCentralWidget(self.table_widget)
    
            self.table_widget.resizeColumnsToContents()
            self.table_widget.resizeRowsToContents()
    
    
    if __name__ == '__main__':
        app = QApplication([])
        window = MainWindow()
        window.show()
        app.exec()