0

How can I make the second column actively in edit mode anytime a row is selected in the tableview as seen in this gif below? I'm trying to recreate this in python/pyside.

Ideally I would like to use some sort of item delegate so i could easily handle the keyPressEvents in the columns cell and add the custom (X) clear button. However I'm not sure how to use delegates like this when using ItemModels. So any help in making this task achievable is appreciated.

class ExampleDelegate(QtGui.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        line_edit = QtGui.QLineEdit(parent)
        return line_edit

enter image description here

Here is my code and a screenshot:

enter image description here

import os, sys
from PySide import QtGui, QtCore


class HotkeyItem():
    def __init__(self, command, shortcut):
        self.command = command
        self.shortcut = shortcut


class HotkeysModel(QtCore.QAbstractTableModel):

    def __init__(self):
        super(HotkeysModel, self).__init__()
        self.items = []
        self.headers = ['Command','Hotkey']

    def clear(self):
        self.beginResetModel()
        self.items = []
        self.endResetModel()

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return len(self.items)

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if orientation == QtCore.Qt.Horizontal:
            if role == QtCore.Qt.DisplayRole:
                cnt = len(self.headers)
                if section < cnt:
                    return self.headers[section]
        return None

    def columnCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return len(self.headers)

    def index(self, row, column, parent=QtCore.QModelIndex()):
        return self.createIndex(row, column, parent)

    def addItem(self, item):
        self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        self.items.append(item)
        self.endInsertRows()

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return

        row = index.row()
        col = index.column()

        if 0 <= row < self.rowCount():
            item = self.items[row]

            if role == QtCore.Qt.DisplayRole:
                if col == 0:
                    return getattr(item, 'command', 'N/A')
                elif col == 1:
                    return getattr(item, 'shortcut', '')

            if role == QtCore.Qt.BackgroundRole:
                shortcuts = filter(None, [x.shortcut for x in self.items])
                dups = shortcuts.count(getattr(item, 'shortcut', ''))
                if dups > 1:
                    return QtGui.QBrush(QtGui.QColor(255, 50, 50, 255))

            elif role == QtCore.Qt.FontRole:
                shortcuts = filter(None, [x.shortcut for x in self.items])
                dups = shortcuts.count(getattr(item, 'shortcut', ''))
                if dups > 1:
                    fnt = QtGui.QFont()
                    fnt.setBold(True)
                    fnt.setItalic(True)
                    return fnt

        return None

    def flags(self, index):
        if not index.isValid():
            return 0
        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable


class Example(QtGui.QWidget):
    def __init__(self, parent=None):
        super(Example, self).__init__(parent)
        self.resize(600, 400)

        model = HotkeysModel()

        proxyModel = QtGui.QSortFilterProxyModel()
        proxyModel.setFilterKeyColumn(0)
        proxyModel.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
        proxyModel.setSourceModel(model)

        self.uiView = QtGui.QTableView()
        self.uiView.setSortingEnabled(True)
        self.uiView.setModel(proxyModel)
        self.uiView.setAlternatingRowColors(True)
        self.uiView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
        self.uiView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
        self.uiView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
        self.uiView.verticalHeader().hide()
        self.uiView.horizontalHeader().show()

        lay = QtGui.QVBoxLayout()
        lay.addWidget(self.uiView)
        self.setLayout(lay)

        self.populate()

        # connections
        selection = self.uiView.selectionModel()
        selection.currentRowChanged.connect(self.selection_changed)


# ui->tableView->setCurrentIndex(index);
# ui->tableView->edit(index);

    def selection_changed(self, index):
        if index.isValid():
            row = index.row()
            self.uiView.setCurrentIndex(index)
            self.uiView.edit(index)


    def populate(self):
        model = self.uiView.model().sourceModel()
        model.clear()

        items = [
            HotkeyItem(command='Save', shortcut='Ctrl+S'),
            HotkeyItem(command='Open', shortcut='Ctrl+O'),
            HotkeyItem(command='Close', shortcut='Ctrl+Q'),
            HotkeyItem(command='Align Top', shortcut=''),
            HotkeyItem(command='Align Bottom', shortcut=''),
            HotkeyItem(command='Align Left', shortcut=''),
            HotkeyItem(command='Align Right', shortcut=''),
            HotkeyItem(command='Align Center', shortcut='Ctrl+O')
        ]

        for x in items:
            model.addItem(x)

        self.uiView.sortByColumn(0, QtCore.Qt.AscendingOrder)
        self.uiView.resizeColumnsToContents()


def main():
    app = QtGui.QApplication(sys.argv)
    ex = Example()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
JokerMartini
  • 5,674
  • 9
  • 83
  • 193

1 Answers1

2

Part of the problem is that for an item to be editable, it must have the flag QtCore.Qt.ItemIsEditable activated. The other part is that the index that passes selection_changed can be from the item in the first column and not from the second, so using that index you should get the index from the second column.

In Qt5 the clear button is already implemented and it is only activated using setClearButtonEnabled(True) and the icon is changed using qss, but in the case of Qt4 it does not exist, so it must be created to use this answer.

Finally you must also implement the setData() method.

import os
import sys
from PySide import QtGui, QtCore


class LineEdit(QtGui.QLineEdit):
    def __init__(self, parent=None):
        super(LineEdit, self).__init__(parent)
        btnSize = self.sizeHint().height() - 5
        self.clearButton = QtGui.QToolButton(self)
        icon = QtGui.QIcon("clear.png")
        self.clearButton.setIcon(icon)
        self.clearButton.setCursor(QtCore.Qt.ArrowCursor)
        self.clearButton.setStyleSheet("QToolButton { border: none; padding: 2px}")
        self.clearButton.setFixedSize(btnSize, btnSize)
        self.clearButton.hide()
        frameWidth = self.style().pixelMetric(QtGui.QStyle.PM_DefaultFrameWidth)
        self.setStyleSheet("QLineEdit{{ padding-right: {}px }}".format(btnSize - frameWidth))
        self.setMinimumHeight(self.sizeHint().height())
        self.clearButton.clicked.connect(self.clear)
        self.textChanged.connect(self.onTextChanged)

    def resizeEvent(self, event):
        frameWidth = self.style().pixelMetric(QtGui.QStyle.PM_DefaultFrameWidth)
        self.clearButton.move(self.width() - self.clearButton.width() - frameWidth, 0)

    def onTextChanged(self, text):
        self.clearButton.setVisible(text != "")


class Delegate(QtGui.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        editor = LineEdit(parent)
        font = index.data(QtCore.Qt.FontRole)
        editor.setFont(font)
        return editor

    def setEditorData(self, editor, index):
        text = index.data()
        editor.setText(text)

    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())

class HotkeyItem():
    def __init__(self, command, shortcut):
        self.command = command
        self.shortcut = shortcut


class HotkeysModel(QtCore.QAbstractTableModel):
    def __init__(self):
        super(HotkeysModel, self).__init__()
        self.items = []
        self.headers = ['Command','Hotkey']

    def clear(self):
        self.beginResetModel()
        self.items = []
        self.endResetModel()

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return len(self.items)

    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if orientation == QtCore.Qt.Horizontal:
            if role == QtCore.Qt.DisplayRole:
                cnt = len(self.headers)
                if section < cnt:
                    return self.headers[section]
        return None

    def columnCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return len(self.headers)

    def addItem(self, item):
        self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        self.items.append(item)
        self.endInsertRows()

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return 

        row = index.row()
        col = index.column()

        if 0 <= row < self.rowCount():
            item = self.items[row]

            if role == QtCore.Qt.DisplayRole:
                if col == 0:
                    return getattr(item, 'command', 'N/A')
                elif col == 1:
                    return getattr(item, 'shortcut', '')

            if role == QtCore.Qt.BackgroundRole:
                shortcuts = filter(None, [x.shortcut for x in self.items])
                dups = shortcuts.count(getattr(item, 'shortcut', ''))
                if dups > 1:
                    return QtGui.QBrush(QtGui.QColor(255, 50, 50, 255))

            elif role == QtCore.Qt.FontRole:
                shortcuts = filter(None, [x.shortcut for x in self.items])
                dups = shortcuts.count(getattr(item, 'shortcut', ''))
                if dups > 1:
                    fnt = QtGui.QFont()
                    fnt.setBold(True)
                    fnt.setItalic(True)
                    return fnt

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if index.isValid():
            row = index.row()
            col = index.column()
            if 0 <= row < self.rowCount() and 0 <= col < self.columnCount():
                it = self.items[row]
                if col == 0:
                    it.command = value
                elif col == 1:
                    it.shortcut = value
                return True
        return False

    def flags(self, index):
        fl = QtCore.Qt.NoItemFlags
        if index.isValid():
            fl |= QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
            if index.column() == 1:
                fl |= QtCore.Qt.ItemIsEditable
        return fl


class Example(QtGui.QWidget):
    def __init__(self, parent=None):
        super(Example, self).__init__(parent)
        self.resize(600, 400)

        model = HotkeysModel()

        proxyModel = QtGui.QSortFilterProxyModel()
        proxyModel.setFilterKeyColumn(0)
        proxyModel.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
        proxyModel.setSourceModel(model)

        self.uiView = QtGui.QTableView()
        self.uiView.setSortingEnabled(True)
        self.uiView.setModel(proxyModel)
        self.uiView.setAlternatingRowColors(True)
        delegate = Delegate(self)
        self.uiView.setItemDelegateForColumn(1, delegate)
        self.uiView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
        self.uiView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
        self.uiView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
        self.uiView.verticalHeader().hide()
        self.uiView.horizontalHeader().show()

        lay = QtGui.QVBoxLayout()
        lay.addWidget(self.uiView)
        self.setLayout(lay)
        self.populate()

        # connections
        selection = self.uiView.selectionModel()
        selection.currentChanged.connect(self.openEditor)
        self.uiView.clicked.connect(self.openEditor)

    def openEditor(self, index):
        if index.isValid():
            ix = index.sibling(index.row(), 1)
            self.uiView.setCurrentIndex(ix)
            self.uiView.edit(ix)

    def populate(self):
        model = self.uiView.model().sourceModel()
        model.clear()

        items = [
            HotkeyItem(command='Save', shortcut='Ctrl+S'),
            HotkeyItem(command='Open', shortcut='Ctrl+O'),
            HotkeyItem(command='Close', shortcut='Ctrl+Q'),
            HotkeyItem(command='Align Top', shortcut=''),
            HotkeyItem(command='Align Bottom', shortcut=''),
            HotkeyItem(command='Align Left', shortcut=''),
            HotkeyItem(command='Align Right', shortcut=''),
            HotkeyItem(command='Align Center', shortcut='Ctrl+O')
        ]

        for x in items:
            model.addItem(x)

        self.uiView.sortByColumn(0, QtCore.Qt.AscendingOrder)
        self.uiView.resizeColumnsToContents()


def main():
    app = QtGui.QApplication(sys.argv)
    ex = Example()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • It works well with a single click, but the edit automatically stops if the user clicks and drags in the first column. And if you click the same cell a 2nd time it also stops the edit. This may not be a big issue for OP, but just a minor ux issue. – Green Cell Sep 17 '18 at 09:07
  • @GreenCell Thanks, I think that I already corregi that problem, tell me if you can if the problem persists. – eyllanesc Sep 17 '18 at 09:46
  • @eyllanesc do you know why it says `edit: editing failed` every time the first column is selected in the tableview? – JokerMartini Sep 17 '18 at 13:02
  • It appears that having both signals for selection change and currentChanged are causing the issue. in the OpenEditor method i need to somehow check if the cell is already in an active Edit state and not to try and set it twice – JokerMartini Sep 17 '18 at 13:44
  • @there seems to be a bug when the user clicks outside the tableview it does not exit editing mode. How can I do that? – JokerMartini Sep 19 '18 at 15:29
  • @JokerMartini I have not had time to review it, in a moment I will. – eyllanesc Sep 19 '18 at 16:47
  • No worries i found a solution by using Commit – JokerMartini Sep 19 '18 at 18:08
  • @JokerMartini It would be great if you show it as an edition of your question or as an additional answer. – eyllanesc Sep 19 '18 at 18:09
  • ill upload it when i get home :) – JokerMartini Sep 19 '18 at 20:57