1

I am trying to restrict selection of a tree to a particular column.

I am using delegates heavily to create custom per-item-per-column behaviors, editors, etc. I was hoping I could somehow do this from a delegate by blocking an event or something similar. The problem is, I think i would have to create an entirely custom solution that mimics extended selection.

However, after a lot of searching and very few examples, it sounds like I want a custom QItemSelectionModel on my tree view. Is this assumption correct?

How do I create a custom QItemSelectionModel that will use the Extended Selection Mode but allow me to ignore or revert a selection if not in a particular column. In other words, clicking on another column should not change the selection (should not select or deselect)

I know how to add the selection model once I have it. I am asking for help implementing the derived class (unless this can be done with a connected signal).

I am using Python, but would value any help.

Thank you,

[EDIT:] I found these similar questions: http://lists.qt.nokia.com/pipermail/qt-interest/2010-September/027647.html

"Subclass QItemSelectionModel and reimplement both select methods to have the behaviour you want. Just ignore the parts of ranges with column > 0. ... Or maybe just reimplement flags() to make the item not selectable. I don't know if that will have any side effects."

I tried reimplementing flags on my QTreeWidgetItem, but it never got called:

def flags(self, index):
    print index.column()
    return super(DDOutlinerBaseItem, self).flags(index)
Rafe
  • 1,937
  • 22
  • 31
  • Do you use a custom model for your data? Wether or not the items are selectable is controlled by the model. See `QAbstractItemModel::flags()` (http://qt-project.org/doc/qt-4.8/qabstractitemmodel.html#flags) and the `Qt::ItemFlags` (http://qt-project.org/doc/qt-4.8/qt.html#ItemFlag-enum). – leemes Jun 06 '12 at 09:50
  • Thank you for the comment. I am using QTreeWidgetItems. I see that `flags` can be read per index, but the only way I found to set flags is at the item level in QTreeWidgetItem.setFlags(). I tried to override the function but it was never called: `def flags(self, index): print index.column() return super(DDOutlinerBaseItem, self).flags(index)` – Rafe Jun 06 '12 at 17:14
  • Ok, this is strange, but Qt's models are anyway :) I just looked into the C++ source code of the selection model. Whenever `select()` is called, it doesn't look even at the associated data model. Try playing with some values for QAbstractItemView.selectionBehaviour (http://qt-project.org/doc/qt-4.8/qabstractitemview.html#SelectionBehavior-enum). Maybe it's the view (not the selection model) which looks at the model when the user wants to select something. If this doesn't help, you need to wait for an answer of someone else, as I don't know enough of selection handling of Qt views. :( – leemes Jun 06 '12 at 17:31
  • Thanks for the effort. I think the issue I am facing is that I need to set these flags at the index level, not the item level, and that doesn't seem to be available. That is why i started looking for ways to block or intercept the behavior. Ideally, my delegate would accept the mouse event so the selection model never even sees it, but I couldn't find that either, so now I'm looking at the model itself. I added some more to my question and I continue to hack my way around. My kingdom for a few examples! – Rafe Jun 06 '12 at 18:30
  • I think item level == index level. An index is defined by its parent item, the row and the column. With "item", I think you exactly mean this precision. It *should* suffice returning a non-selectable flag for any requests made to the flag() method, when the column of the given index is one of your columns which should not accept the selection. But I think you got that already -- there has to be another mistake. – leemes Jun 06 '12 at 18:34
  • How would that look when populating the tree? I'm pretty new to Qt and am forming my own mental map of how to use it. It seems to me a QTreeWidgetItem is a row with a parent. You can get an item from an index or index from an item, but the index carries the column where the item doesn't seem to care. These things lead me to believe the index is the "thing" at a particular column for an item (parent & row, column). Can a QTreeWidgetItem also be a column of an another QTreeWidgetItem? This doesn't affect the solution I chose, since it gives more flexibility, but it is interesting. – Rafe Jun 06 '12 at 19:36
  • An item is something like a "cell" in a table (like in a excel sheet). As far as I know, every cell can have a whole table as its children (think about a tree, where each node is a excel sheet; every cell has its own child-table). This is the most generic ("abstract") item model you can think about, but in most cases, you have either only one column (= simple tree of items), no children (= single table) or at least no multiple columns as children (= table with child-*rows*). I think that in the latter case, `column==0` for children. See http://qt-project.org/doc/qabstractitemmodel.html#details – leemes Jun 06 '12 at 20:01

2 Answers2

5

The following adjustment should work, in theory.

The above solution could use two separate methods and @pyqtSlot decorators to disambiguate the overloaded method names:

@pyqtSlot(QModelIndex, QItemSelectionModel.SelectionFlags)
  def select(self, index, command):
    # ...

@pyqtSlot(QItemSelection, QItemSelectionModel.SelectionFlags)
  def select(self, selection, command):
    #...

This avoids the need to check for instances of certain classes in the method implementations.

David Boddie
  • 66
  • 1
  • 3
0

The first interesting thing is that since Python can't overload a method, my select method seems to simply be called twice, once for each type in argument 0. Here is an example to illustrate this as well as the basic setup. My QTreeWidget's tree it called 'tree' (self.tree)

    # in __init__ of my QTreeWidget:
    sel_model = ColumnSelectionModel(self.tree.model())
    self.tree.setSelectionModel(sel_model)

class ColumnSelectionModel(QtGui.QItemSelectionModel):
    def select(self, selection, selectionFlags):
    """
    Runs both QItemSelectionModel.select methods::

        1. select(QtCore.QModelIndex, QItemSelectionModel.SelectionFlags)
        2. select(QtGui.QItemSelection, QItemSelectionModel.SelectionFlags)

    The first seems to run on mouse down and mouse up.
    The second seems to run on mouse down, up and drag
    """
    print("select(%s,  %s)" % (type(selection), type(selectionFlags)))

    if isinstance(selection, QtGui.QItemSelection):
        infos = []
        for index in selection.indexes():
            infos.append(("index=%s row=%s column=%s" 
                                    % (index, index.row(), index.column())))

        print ", ".join(infos)
    elif isinstance(selection, QtCore.QModelIndex):
        index = selection
        print("index=%s row=%s column=%s" % (index, index.row(), index.column()))
    else:
        raise Exception("Unexpected type for arg 0: '%s'" % type(selection))

    super(ColumnSelectionModel, self).select(selection, selectionFlags)

This appears to solve my problem:

class ColumnSelectionModel(QtGui.QItemSelectionModel):
    def __init__(self, model):
        super(ColumnSelectionModel, self).__init__(model)

        self.selectable_columns = [0]
        """ Set the columns that are allowed to be selected """


    def select(self, selection, selectionFlags):
        """
        Ignores any selection changes if an item is not in one of the columns
        in the self.selectable_columns list.

        Is run by both QItemSelectionModel.select methods::

            1. select(QtCore.QModelIndex, QItemSelectionModel.SelectionFlags)
            2. select(QtGui.QItemSelection, QItemSelectionModel.SelectionFlags)

        The first seems to run on mouse down and mouse up.
        The second seems to run on mouse down, up and drag
        """
        if isinstance(selection, QtGui.QItemSelection):
            # This is the overload with the QItemSelection passed to arg 0
            # Loop over all the items and if any are not in selectable_columns
            #   ignore this event. This works because it is run for every change
            #   so the offending selection index will always be the newest
            indexes = selection.indexes()
            for i in xrange(len(indexes)):
                index = indexes[i]
                if not index.column() in self.selectable_columns:
                    return
        elif isinstance(selection, QtCore.QModelIndex):
            # This is the overload with the QModelIndex passed to arg 0
            # If this index isn't in selectable_columns, just ignore this event
            index = selection
            if not index.column() in self.selectable_columns:
                return
        else:  # Just in case
            raise Exception("Unexpected type for arg 0: '%s'" % type(selection))

        # Fall through. Select as normal
        super(ColumnSelectionModel, self).select(selection, selectionFlags)

In my final implementation, I plan to delegate the decision to my delegate system, making this generic and, in theory, able to dynamically ignore any index I want.

Rafe
  • 1,937
  • 22
  • 31