jfsturtz
jfsturtz

Reputation: 437

QTableView -- prevent departure from cell and closure of Delegate editor

I have a QAbstractTableModel+QTableView, and a Delegate assigned which creates a QLabel widget to use as the editor.

I simply want to do this: When the Delegate editor is active, under certain circumstances (when the data in the cell doesn't validate), inhibit departure from the cell and stay in the editing session. In other words, if circumstances dictate and the user tries to leave the cell (by whatever means -- tab, arrow key, mouse click, etc), don't do anything at all. Just stay put, as though nothing ever happened.

I thought this would be easy, but I have not been able to figure out how to do it.

My first thought was that I could catch the Delegate's closeEditor signal. That code is shown below. It's a bit long (so as to be standalone executable), but most of what's shown is just standard model/view/delegate stuff. The interesting part is at the bottom. I've defined a slot (on_closeEditor()), and connected it to the closeEditor signal (see the ### ... ### comments).

The Delegate catches when the Enter key is pressed and emits the closeEditor signal explicitly. When that happens, the on_closeEditor() slot gets called. So the connection seems to be made properly.

But when the cell is departed by other means (e.g,. tab key or mouse click), although the Delegate editor does appear to be closed, the slot never gets called.

(There's also the matter that, even if my code could gain control when a Delegate editor is closing, it's not clear to me how I'd stop it from happening. But one thing at a time ...)

Is there a straightforward way to do this? I feel like I must be missing something ...

Thanks!

Sample Code

from PyQt5 import QtCore, QtWidgets, QtGui
import sys


# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data = [[]], headers = None, parent = None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.__data = data

    def rowCount(self, parent):
        return len(self.__data)

    def columnCount(self, parent):
        return len(self.__data[0])

    def data(self, index, role):
        row = index.row()
        column = index.column()
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            value = self.__data[row][column]
            return value
        if role == QtCore.Qt.BackgroundRole:
            return QtGui.QBrush(QtGui.QColor(230, 240, 250))


    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            if value is None:
                value = ''
            self.__data[row][column] = value
            return True
        return False


    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable


# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.blocked = False

    def keyPressEvent(self, event):
        key = event.key()
        mod = int(event.modifiers())
        row = self.currentIndex().row()

        if key == QtCore.Qt.Key_Q and mod == QtCore.Qt.CTRL:
            self.close()
            exit()

        super().keyPressEvent(event)


# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):

    def createEditor(self, parent, option, index):
        self.editor = QtWidgets.QLabel(parent)
        return self.editor

    def setEditorData(self, label, index):
        print('setEditorData()')
        model = index.model()
        v = model.data(index, QtCore.Qt.EditRole)
        model.setData(index, None, QtCore.Qt.EditRole)

    def setModelData(self, label, model, index):
        print('setModelData()')
        value = label.text()
        row = index.row()
        col = index.column()
        model.setData(index, value, QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def eventFilter(self, target, event):
        if event.type() == QtCore.QEvent.KeyPress:
            key = event.key()
            mod = int(event.modifiers())

            if (
                key >= QtCore.Qt.Key_Space and key <= QtCore.Qt.Key_AsciiTilde and 
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.SHIFT)
            ):
                text = self.editor.text()
                self.editor.setText(text + event.text())
                return True

            # Enter (or ctrl-Enter) explicitly emits commitData, closeEditor
            elif (
                key == QtCore.Qt.Key_Return and
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.CTRL)
            ):
                self.commitData.emit(target)
                self.closeEditor.emit(target)
                return True

        return False

    ### closeEditor slot ###
    def on_closeEditor(self, editor, hint):
        print('closeEditor()')


# ------------------------------------------------------------------------------
if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('fusion')

    tableView = TableView()
    tableView.resize(550, 160)

    delegate = Delegate()
    tableView.setItemDelegate(delegate)

    ### connect closeEditor signal to slot ###
    delegate.closeEditor.connect(delegate.on_closeEditor)

    tableView.show()

    rowCount = 3
    columnCount = 4
    data = [
        ['foo', 'goo', 'zoo', 'moo'],
        ['bar', 'zar', 'jar', 'gar'],
        ['qux', 'lux', 'mux', 'sux']
        ]

    model = TableModel(data)
    tableView.setModel(model)

    sys.exit(app.exec_())

[edit]

My next thought was that I could install an event filter for the Delegate, and filter out the FocusAboutToChange and/or FocusOut events. In fact, I really thought this was going to be the perfect solution.

But it didn't work. :-(

print() statements show that the events are properly detected. I thought if eventFilter() returned True, the events would be stopped. But it seems not to be so. The cursor leaves the edited cell anyhow.

Code

from PyQt5 import QtCore, QtWidgets, QtGui
import sys


# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data = [[]], headers = None, parent = None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.__data = data

    def rowCount(self, parent):
        return len(self.__data)

    def columnCount(self, parent):
        return len(self.__data[0])

    def data(self, index, role):
        row = index.row()
        column = index.column()
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            value = self.__data[row][column]
            return value
        if role == QtCore.Qt.BackgroundRole:
            return QtGui.QBrush(QtGui.QColor(230, 240, 250))

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            if value is None:
                value = ''
            self.__data[row][column] = value
            return True
        return False

    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable


# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.blocked = False

    def keyPressEvent(self, event):
        key = event.key()
        mod = int(event.modifiers())

        if key == QtCore.Qt.Key_Q and mod == QtCore.Qt.CTRL:
            self.close()
            exit()

        super().keyPressEvent(event)


# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):

    def createEditor(self, parent, option, index):
        self.editor = QtWidgets.QLabel(parent)
        return self.editor

    def setEditorData(self, label, index):
        model = index.model()
        v = model.data(index, QtCore.Qt.EditRole)
        model.setData(index, None, QtCore.Qt.EditRole)

    def setModelData(self, label, model, index):
        value = label.text()
        row = index.row()
        col = index.column()
        model.setData(index, value, QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def eventFilter(self, target, event):

        if event.type() == QtCore.QEvent.KeyPress:
            key = event.key()
            mod = int(event.modifiers())

            # ASCII input
            if (
                key >= QtCore.Qt.Key_Space and key <= QtCore.Qt.Key_AsciiTilde and 
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.SHIFT)
            ):
                text = self.editor.text()
                self.editor.setText(text + event.text())
                return True

        ### Ostensibly filter out FocusAboutToChange and FocusOut events ###
        if event.type() == QtCore.QEvent.FocusAboutToChange:
            print('FocusAboutToChange')
            return True
        if event.type() == QtCore.QEvent.FocusOut:
            print('FocusOut')
            return True

        return False


# ------------------------------------------------------------------------------
if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('fusion')

    rowCount = 3
    columnCount = 4
    data = [
        ['foo', 'goo', 'zoo', 'moo'],
        ['bar', 'zar', 'jar', 'gar'],
        ['qux', 'lux', 'mux', 'sux']
        ]

    tableView = TableView()
    tableView.resize(550, 160)

    delegate = Delegate()
    tableView.setItemDelegate(delegate)
    delegate.installEventFilter(delegate)

    tableView.show()
    model = TableModel(data)
    tableView.setModel(model)

    sys.exit(app.exec_())

Upvotes: 2

Views: 1466

Answers (1)

jfsturtz
jfsturtz

Reputation: 437

In case anyone is following this and is curious how it worked out:

With help from some guys at the Riverbank Computing PyQt mailing list, I arrived at the following solution. In involves the following:

If the data in the cell doesn't validate:

  • Override the Delegate's setModelData() method and suppress posting the cell data to the model. Use label.setFocus() to keep focus on the editor widget, and view.setCurrentIndex() to keep the view's current index from changing.

  • Override the View's closeEditor() slot to prevent the editor from closing.

Solution is shown below.

from PyQt5 import QtCore, QtWidgets, QtGui
import sys, re


# ------------------------------------------------------------------------------
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data = [[]], headers = None, parent = None):
        QtCore.QAbstractTableModel.__init__(self, parent)
        self.__data = data
    def rowCount(self, parent):
        return len(self.__data)
    def columnCount(self, parent):
        return len(self.__data[0])
    def data(self, index, role):
        row = index.row()
        column = index.column()
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            value = self.__data[row][column]
            return value
        if role == QtCore.Qt.BackgroundRole:
            return QtGui.QBrush(QtGui.QColor(230, 240, 250))
    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            row = index.row()
            column = index.column()
            if value is None:
                value = ''
            self.__data[row][column] = value
            return True
        return False
    def flags(self, index):
        return QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsEditable


# ------------------------------------------------------------------------------
class TableView(QtWidgets.QTableView):

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

    def keyPressEvent(self, event):
        key = event.key()
        mod = int(event.modifiers())
        if key == QtCore.Qt.Key_Q and mod == QtCore.Qt.CTRL:
            self.close()
            exit()
        super().keyPressEvent(event)

    def closeEditor(self, editor, hint):

        ### --- If data validates, close editor; otherwise, don't --- ###
        if editor.validate():
            print(f'>> Closing editor')
            super().closeEditor(editor, hint)
        else:
            print(f'>> Not closing editor')


# ------------------------------------------------------------------------------
class Delegate(QtWidgets.QStyledItemDelegate):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.editor = None

    def createEditor(self, parent, option, index):
        self.view = parent.parent()
        self.editor = CellEditor(parent)
        return self.editor

    def setEditorData(self, label, index):
        model = index.model()
        v = model.data(index, QtCore.Qt.EditRole)

    def setModelData(self, label, model, index):
        value = label.text()
        row = index.row()
        col = index.column()

        ### --- If data validates, post it to the model; otherwise, don't --- ###
        if label.validate():
            model.setData(index, value, QtCore.Qt.EditRole)
            print(f'>> [setModelData({value})] accepted')
        else:
            label.setFocus()
            self.view.setCurrentIndex(index)
            print(f'>> [setModelData({value})] rejected')

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def eventFilter(self, target, event):
        if event.type() == QtCore.QEvent.KeyPress:
            key = event.key()
            mod = int(event.modifiers())

            # ASCII input -- add to cell value
            if (
                key >= QtCore.Qt.Key_Space and key <= QtCore.Qt.Key_AsciiTilde and 
                (mod == QtCore.Qt.NoModifier or mod == QtCore.Qt.SHIFT)
            ):
                text = self.editor.text()
                self.editor.setText(text + event.text())
                return True

            # [ctrl-H], Backspace -- delete a character
            elif (
                (key == QtCore.Qt.Key_H and mod == QtCore.Qt.CTRL) or
                (key == QtCore.Qt.Key_Backspace and mod == QtCore.Qt.NoModifier)
            ):
                self.editor.setText(self.editor.text()[:-1])
                return True

        return False


# ------------------------------------------------------------------------------
class CellEditor(QtWidgets.QLabel):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setStyleSheet('font-style: italic; font-weight: bold; color: blue')
        self.setAutoFillBackground(True)

    ### --- Sample validation function --- ###
    def validate(self):
        return re.fullmatch('\d+', self.text())



# ------------------------------------------------------------------------------
if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('fusion')

    rowCount = 3
    columnCount = 4
    data = [
        ['foo', 'goo', 'zoo', 'moo'],
        ['bar', 'zar', 'jar', 'gar'],
        ['qux', 'lux', 'mux', 'sux']
        ]

    view = TableView()
    view.resize(550, 160)
    model = TableModel(data)
    view.setModel(model)
    view.show()

    delegate = Delegate()
    view.setItemDelegate(delegate)

    sys.exit(app.exec_())

Upvotes: 2

Related Questions