QTreeView only edits in first column?
Question: I am trying to make a simple property editor, where the property list is a
nested dict and the data is displayed and edited in a QTreeView. (Before I get
to my question -- if anyone already has a working implementation of this in
Python 3 I'd love to be pointed at it).

Anyway, after much work I have my QAbstractItemModel and I can open a
QTreeView with this model and it shows the data. If I click on a label in the
first column (the key) then it opens up an editor, either a text editor or a
spinbox etc depending on the datatype. When I finish editing it calls my
"model.setData" where I reject it because I don't want to allow editable keys.
I can disable the editing of this by using flags and that works fine. I just
wanted to check that everything works the way that I'd expect it to.

Here is what doesn't happen: if I click on a cell in the second column (the value that I actually want to edit) then it bypasses the loading of an editor and simply calls model.setData with the current value. I am baffled. I've tried changing the tree selectionBehavior and selectionMode but no dice. I'm returning Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable in flags. It seems to display fine. It just won't open up an editor.

Any thoughts about what stupid mistake I must be making? I'll include the code
below, with some print statements that I'm using to try to debug the thing.

Thanks

PS One thing that hung me up for a long time was that my QModelIndex members
would just disappear, so the indices that I got back were garbage. I found
that by keeping a reference to them (throwing them in a list) that they
worked. This seems to be a problem that springs up a lot in Qt work (I had the
same problem with menus disappearing -- I guess that means that I should think
about it sooner). Is there a "best practices" way of dealing with this?

    
    
    # -*- coding: utf-8 -*-
    
    from collections import OrderedDict
    from PyQt4.QtCore import QAbstractItemModel, QModelIndex, Qt
    from PyQt4.QtGui import QAbstractItemView
    
    class PropertyList(OrderedDict):
        def __init__(self, *args, **kwargs):
            OrderedDict.__init__(self, *args, **kwargs)
            self.myModel = PropertyListModel(self)
    
        def __getitem__(self,index):
            if issubclass(type(index), list):
                item = self
                for key in index:
                    item = item[key]
                return item
            else:
                return OrderedDict.__getitem__(self, index)
    
    
    class PropertyListModel(QAbstractItemModel):
    
        def __init__(self, propList, *args, **kwargs):
            QAbstractItemModel.__init__(self, *args, **kwargs)
            self.propertyList = propList
            self.myIndexes = []   # Needed to stop garbage collection
    
        def index(self, row, column, parent):
            """Returns QModelIndex to row, column in parent (QModelIndex)"""
            if not self.hasIndex(row, column, parent):
                return QModelIndex()        
            if parent.isValid():
                indexPtr = parent.internalPointer()
                parentDict = self.propertyList[indexPtr]
            else:
                parentDict = self.propertyList
                indexPtr = []
            rowKey = list(parentDict.keys())[row]
            childPtr = indexPtr+[rowKey]
            newIndex = self.createIndex(row, column, childPtr)
            self.myIndexes.append(childPtr)
            return newIndex
    
        def get_row(self, key):
            """Returns the row of the given key (list of keys) in its parent"""
            if key:
                parent = key[:-1]
                return list(self.propertyList[parent].keys()).index(key[-1])
            else:
                return 0
    
        def parent(self, index):
            """
            Returns the parent (QModelIndex) of the given item (QModelIndex)
            Top level returns QModelIndex()
            """
            if not index.isValid():
                return QModelIndex()
            childKeylist = index.internalPointer()
            if childKeylist:
                parentKeylist = childKeylist[:-1]
                self.myIndexes.append(parentKeylist)
                return self.createIndex(self.get_row(parentKeylist), 0,
                                        parentKeylist)
            else:
                return QModelIndex()
    
        def rowCount(self, parent):
            """Returns number of rows in parent (QModelIndex)"""
            if parent.column() > 0:
                return 0    # only keys have children, not values
            if parent.isValid():
                indexPtr = parent.internalPointer()
                try:
                    parentValue = self.propertyList[indexPtr]
                except:
                    return 0
                if issubclass(type(parentValue), dict):
                    return len(self.propertyList[indexPtr])
                else:
                    return 0
            else:
                return len(self.propertyList)
    
        def columnCount(self, parent):
            return 2  # Key & value
    
        def data(self, index, role):
            """Returns data for given role for given index (QModelIndex)"""
           # print('Looking for data in role {}'.format(role))
            if not index.isValid():
                return None
            if role in (Qt.DisplayRole, Qt.EditRole):
                indexPtr = index.internalPointer()
                if index.column() == 1:    # Column 1, send the value
                    return self.propertyList[indexPtr]
                else:                   # Column 0, send the key
                    if indexPtr:
                        return indexPtr[-1]
                    else:
                        return ""
            else:  # Not display or Edit
                return None
    
        def setData(self, index, value, role):
            """Sets the value of index in a given role"""
            print('In SetData')
            if not index.isValid():
                return False
            print('Trying to set {} to {}'.format(index,value))
            print('That is column {}'.format(index.column()))
            if not index.column():  # Only change column 1
                return False
            try:
                ptr = index.internalPointer()
                self.propertyList[ptr[:-1]][ptr[-1]] = value
                self.emit(self.dataChanged(index, index))
                return True
            except:
                return False
    
        def flags(self, index):
            """Indicates what can be done with the data"""
            if not index.isValid():
                return Qt.NoItemFlags
            if index.column():  # only enable editing of values, not keys
                return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
            else:
                return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable #Qt.NoItemFlags
    
    if __name__ == '__main__':
        p = PropertyList({'k1':'v1','k2':{'k3':'v3','k4':4}})
    
        import sys
        from PyQt4 import QtGui
        qApp = QtGui.QApplication(sys.argv)
    
        treeView = QtGui.QTreeView()
    
    # I've played with all the settings on these to no avail
        treeView.setHeaderHidden(False)
        treeView.setAllColumnsShowFocus(True)
        treeView.setUniformRowHeights(True)
        treeView.setSelectionBehavior(QAbstractItemView.SelectRows)
        treeView.setSelectionMode(QAbstractItemView.SingleSelection)
        treeView.setAlternatingRowColors(True)
        treeView.setEditTriggers(QAbstractItemView.DoubleClicked | 
                                 QAbstractItemView.SelectedClicked |
                                 QAbstractItemView.EditKeyPressed |
                                 QAbstractItemView.AnyKeyPressed)
        treeView.setTabKeyNavigation(True)                             
        treeView.setModel(p.myModel)
        treeView.show()
    
        sys.exit(qApp.exec_())
    


Answer: @strubbly was real close but forgot to unpack the tuple in his `index` method.

Here's the working code for Qt5. There are probably a couple of imports and
stuff that would need to be fixed. Only cost me a couple weeks of my life :)

    
    
    import sys
    from collections import OrderedDict
    from PyQt5 import QtCore, QtWidgets
    from PyQt5.QtCore import Qt
    
    class TupleKeyedOrderedDict(OrderedDict):
        def __init__(self, *args, **kwargs):
            super().__init__(sorted(kwargs.items()))
    
        def __getitem__(self, key):
            if isinstance(key, tuple):
                item = self
                for k in key:
                    if item != ():
                        item = item[k]
                return item
            else:
                return super().__getitem__(key)
    
        def __setitem__(self, key, value):
            if isinstance(key, tuple):
                item = self
                previous_item = None
                for k in key:
                    if item != ():
                        previous_item = item
                        item = item[k]
                previous_item[key[-1]] = value
            else:
                return super().__setitem__(key, value)
    
    class SettingsModel(QtCore.QAbstractItemModel):
        def __init__(self, data, parent=None):
            super().__init__(parent)
            self.root = data
            self.my_index = {}   # Needed to stop garbage collection
    
        def index(self, row, column, parent):
            if not self.hasIndex(row, column, parent):
                return QtCore.QModelIndex()
            if parent.isValid():
                index_pointer = parent.internalPointer()
                parent_dict = self.root[index_pointer]
            else:
                parent_dict = self.root
                index_pointer = ()
            row_key = list(parent_dict.keys())[row]
            child_pointer = (*index_pointer, row_key)
            try:
                child_pointer = self.my_index[child_pointer]
            except KeyError:
                self.my_index[child_pointer] = child_pointer
            index = self.createIndex(row, column, child_pointer)
            return index
    
        def get_row(self, key):
            if key:
                parent = key[:-1]
                if not parent:
                    return 0
                return list(self.root[parent].keys()).index(key[-1])
            else:
                return 0
    
        def parent(self, index):
            if not index.isValid():
                return QtCore.QModelIndex()
            child_key_list = index.internalPointer()
            if child_key_list:
                parent_key_list = child_key_list[:-1]
                try:
                    parent_key_list = self.my_index[parent_key_list]
                except KeyError:
                    self.my_index[parent_key_list] = parent_key_list
                return self.createIndex(self.get_row(parent_key_list), 0,
                                        parent_key_list)
            else:
                return QtCore.QModelIndex()
    
        def rowCount(self, parent):
            if parent.column() > 0:
                return 0    # only keys have children, not values
            if parent.isValid():
                indexPtr = parent.internalPointer()
                parentValue = self.root[indexPtr]
                if isinstance(parentValue, OrderedDict):
                    return len(self.root[indexPtr])
                else:
                    return 0
            else:
                return len(self.root)
    
        def columnCount(self, parent):
            return 2  # Key & value
    
        def data(self, index, role):
            if not index.isValid():
                return None
            if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
                indexPtr = index.internalPointer()
                if index.column() == 1:    # Column 1, send the value
                    return self.root[indexPtr]
                else:                   # Column 0, send the key
                    if indexPtr:
                        return indexPtr[-1]
                    else:
                        return None
            else:  # Not display or Edit
                return None
    
        def setData(self, index, value, role):
            pointer = self.my_index[index.internalPointer()]
            self.root[pointer] = value
            self.dataChanged.emit(index, index)
            return True
    
        def flags(self, index):
            if not index.isValid():
                return 0
            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)
        data = TupleKeyedOrderedDict(**{'1': OrderedDict({'sub': 'b'}), '2': OrderedDict({'subsub': '3'})})
    
        model = SettingsModel(data)
        tree_view = QtWidgets.QTreeView()
        tree_view.setModel(model)
        tree_view.show()
        sys.exit(app.exec_())
    

