Question

I'm building a python/PySide tool for cinematography. I created an object which represents a shot. It has properties for start time, end time, and a list of references to actor objects. The actor object has simple properties (name, build, age, etc) and can be shared between shots.

I want to display this in two table views in PySide. One table view lists the shots (and properties in columns), while the other displays actors referenced in the selected shots. If no shots are selected, the second table view is empty. If multiple shots are selected, all referenced actors are displayed in the actor table view.

I created an abstractTableModel for my shot data and everything is working correctly for the shot data in its corresponding table view. However I'm not sure how to even approach the table view for the actors. Should I use another abstractTableModel for the actors? I can't seem to figure out how to feed/connect the data to the second table view for actors contained in selected shots using a abstractTableModel.

I think part of my issue is that I only want to display the actor information once, regardless of whether multiple selected shots reference the same actor. After multiple failed attempts, I'm thinking that I need to redirect and parse the selected shot information (their actor list property) into a custom property of the main window to contain a list of all referenced actor indices and make an abstractTableModel which uses this to fetch the actual actor properties for display. I'm not fully confident this will work, not to mention my gut tells me this is a messy approach, so I've come here for advice.

Is this the correct approach? If not, what's the 'correct' way of setting this up in PySide/python.

Please keep in mind this is my first foray in data models and PySide.

Here's the shot model.

class ShotTableModel(QtCore.QAbstractTableModel):
    def __init__(self, data=[], parent=None, *args):
        super(ShotTableModel, self).__init__(parent)
        self._data = data

    def rowCount(self, parent):
        return len(self._data.shots)

    def columnCount(self, parent):
        return len(self._data._headers_shotList)

    def getItemFromIndex(self, index):
        if index.isValid():
            item = self._data.shots[index.row()]   
            if item:
                return item
        return None

    def flags(self, index):
        if index.isValid():
            item = self.getItemFromIndex(index)
            return item.qt_flags(index.column())

    def data(self, index, role):
        if not index.isValid():
            return None

        item = self.getItemFromIndex(index)

        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            return item.qt_data(index.column())

        if role == QtCore.Qt.CheckStateRole:
            if index.column() is 0:
                return item.qt_checked

#         if role == QtCore.Qt.BackgroundColorRole:
#             return QtGui.QBrush()

#         if role == QtCore.Qt.FontRole:
#             return QtGui.QFont()

        if role == QtCore.Qt.DecorationRole:
            if index.column() == 0:
                resource = item.qt_resource()
                return QtGui.QIcon(QtGui.QPixmap(resource))

        if role == QtCore.Qt.ToolTipRole:
            return item.qt_toolTip()

        return None

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if index.isValid():
            item = self.getItemFromIndex(index)
            if role == QtCore.Qt.EditRole:
                item.qt_setData(index.column(), value)
                self.dataChanged.emit(index, index)
                return value

            if role == QtCore.Qt.CheckStateRole:
                if index.column() is 0:
                    item.qt_checked = value
                    return True
        return value

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self._data._headers_shotList[section]

    def insertRows(self, position, rows, parent = QtCore.QModelIndex()):
        self.beginInsertRows(parent, position, position + rows - 1)
        for row in range(rows):
            newShotName = self._data.getUniqueName(self._data.shots, 'New_Shot')
            newShot = Shot(newShotName)
            self._data.shots.insert(position, newShot)
        self.endInsertRows()
        return True

    def removeRows(self, position, rows, parent = QtCore.QModelIndex()):
        self.beginRemoveRows(parent, position, position + rows - 1)
        for row in range(rows):
            self._data.shots.pop(position)   
        self.endRemoveRows()
        return True

Here's the data block containing the shot and actor instances. This is what I pass to the shot model.

class ShotManagerData(BaseObject):
    def __init__(self, name='', shots=[], actors=[]):
        super(ShotManagerData, self).__init__(name)
        self._shots = shots # Shot(name="New_Shot", start=0, end=100, actors=[])
        self._actors = actors # Actor(name="New_Actor", size="Average", age=0)

        self.selectedShotsActors = [] #decided to move to this data block

        self._headers_shotList = ['Name', 'Start Frame', 'End Frame']
        self._headers_actorList = ['Name', 'Type', 'RootNode', 'File']

    def save(self, file=None):
        mEmbed.save('ShotManagerData', self)

    @classmethod
    def load(cls, file=None):
        return(mEmbed.load('ShotManagerData'))

    @staticmethod
    def getUniqueName(dataList, baseName='New_Item'):
        name = baseName
        increment = 0
        list_of_names = [data.name if issubclass(data.__class__, BaseObject) else str(data) for data in dataList] 
        while name in list_of_names:
            increment += 1
            name = baseName + '_{0:02d}'.format(increment)
        return name

    @property
    def actors(self):
        return self._actors

    @property
    def shots(self):
        return self._shots

    def actorsOfShots(self, shots):
        actorsOfShots = []
        for shot in shots:
            for actor in shot.actors:
                if actor not in actorsOfShots:
                    actorsOfShots.append(actor)
        return actorsOfShots

    def shotsOfActors(self, actors):
        shotsOfActors = []
        for actor in actors:
            for shot in self.shots:
                if actor in shot.actors and actor not in shotsOfActors:
                    shotsOfActors.append(shot)
        return shotsOfActors

Finally the main tool.

class ShotManager(form, base):
    def __init__(self, parent=None):
        super(ShotManager, self).__init__(parent)
        self.setupUi(self)

        #=======================================================================
        # Properties
        #=======================================================================


        self.data = ShotManagerData() #do any loading if necessary here
        self.actorsInSelectedShots = []

        #test data
        actor1 = Actor('Actor1')
        actor2 = Actor('Actor2')
        actor3 = Actor('Actor3')

        shot1 = Shot('Shot1', [actor1, actor2])
        shot2 = Shot('Shot2', [actor2, actor3])
        shot3 = Shot('Shot3', [actor1])

        self.data.actors.append(actor1)
        self.data.actors.append(actor2)   
        self.data.actors.append(actor3)

        self.data.shots.append(shot1)
        self.data.shots.append(shot2)
        self.data.shots.append(shot3)

        #=======================================================================
        # Models
        #=======================================================================
        self._model_shotList = ShotTableModel(self.data)
        self._proxyModel_shotList = QtGui.QSortFilterProxyModel()
        self._proxyModel_shotList.setSourceModel(self._model_shotList)
        self.shotList.setModel(self._proxyModel_shotList)  #this is the QTableView
        self._selModel_shotList = self.shotList.selectionModel()
        self.shotList.setSortingEnabled(True)
        self._proxyModel_shotList.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)


        self._model_actorList = SelectedShotsActorTableModel(self.data)
        self._proxyModel_actorList = QtGui.QSortFilterProxyModel()
        self._proxyModel_actorList.setSourceModel(self._model_actorList)
        self.actorList.setModel(self._proxyModel_actorList)
        self._selModel_actorList = self.actorList.selectionModel()

        #=======================================================================
        # Events
        #=======================================================================
        self.addShot.clicked.connect(self.addShot_clicked)
        self.delShot.clicked.connect(self.delShot_clicked)


        self._selModel_shotList.selectionChanged.connect(self.shotList_selectionChanged)

    #===========================================================================
    # General Functions    
    #===========================================================================
    def getSelectedRows(self, widget):
        selModel = widget.selectionModel()
        proxyModel = widget.model()
        model = proxyModel.sourceModel()
        rows = [proxyModel.mapToSource(index).row() for index in selModel.selectedRows()]
        rows.sort()
        return rows

    def getSelectedItems(self, widget):
        selModel = widget.selectionModel()
        proxyModel = widget.model()
        model = proxyModel.sourceModel()
        indices = [proxyModel.mapToSource(index) for index in selModel.selectedRows()]
        items = [model.getItemFromIndex(index) for index in indices]
        return items
    #===========================================================================
    # Event Functions    
    #===========================================================================
    def addShot_clicked(self):
        position = len(self.data.shots)
        self._proxyModel_shotList.insertRows(position,1)

    def delShot_clicked(self):
        rows = self.getSelectedRows(self.shotList)
        for row in reversed(rows):
            self._proxyModel_shotList.removeRows(row, 1)

    def shotList_selectionChanged(self, selected, deselected):
        selectedShots = self.getSelectedItems(self.shotList)
        print 'SelectedShots: {}'.format(selectedShots)
        self.data.selectedShotsActors = self.data.actorsOfShots(selectedShots)
        print 'ActorsOfShots: {}'.format(self.data.selectedShotsActors)

        self._proxyModel_actorList.setData() # this line reports missing variables

This is the selectedShotActors model:

class SelectedShotsActorTableModel(QtCore.QAbstractTableModel):
    def __init__(self, data=[], headers=[], parent=None, *args):
        super(SelectedShotsActorTableModel, self).__init__(parent)
        self._data = data

    def rowCount(self, parent):
        return len(self._data.selectedShotsActors)

    def columnCount(self, parent):
        return len(self._data._headers_actorList)

    def getItemFromIndex(self, index):
        if index.isValid():
            item = self._data.selectedShotsActors[index.row()]   
            if item:
                return item
        return None

    def flags(self, index):
        if index.isValid():
            item = self.getItemFromIndex(index)
            return item.qt_flags(index.column())

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

        item = self.getItemFromIndex(index)
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            return item.qt_data(index.column())

        if role == QtCore.Qt.CheckStateRole:
            if index.column() is 0:
                return item.qt_checked
#         
#         if role == QtCore.Qt.BackgroundColorRole:
#             return QtGui.QBrush()
#         
#         if role == QtCore.Qt.FontRole:
#             return QtGui.QFont()

        if role == QtCore.Qt.DecorationRole:
            if index.column() == 0:
                resource = item.qt_resource()
                return QtGui.QIcon(QtGui.QPixmap(resource))

        if role == QtCore.Qt.ToolTipRole:
            return item.qt_toolTip()

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if index.isValid():
            item = self.getItemFromIndex(index)
            if role == QtCore.Qt.EditRole:
                item.qt_setData(index.column(), value)
                self.dataChanged.emit(index, index)
                return value
            if role == QtCore.Qt.CheckStateRole:
                if index.column() is 0:
                    item.qt_checked = value
                    return True
        return value

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                return self._data._headers_actorList[section]
Was it helpful?

Solution

I would suggest that you use a proxy model. Proxy models do not hold data, they link to an existing model and provide the opportunity to sort, filter or restructure that data as necessary.

Specifically, you can create a QSortFilterProxyModel object with the QTableView's QItemSelectionModel as the source. You can then create a custom filter that constructs a list of actors based on the selected shots. The advantage of this approach is that the proxy model and the view will automatically update as the selection changes. I think this approach adheres to the intent of MVC better than adding code to MainWindow.

See the Custom Sort/Filter Model example for more information on how to do this. http://qt-project.org/doc/qt-5/qtwidgets-itemviews-customsortfiltermodel-example.html

If you need some background information (I am new to Qt MVC too; I feel your pain) here are some other useful links:

Model-View Programming: Proxy models http://qt-project.org/doc/qt-5/model-view-programming.html#proxy-models

QSortFilterProxyModel http://qt-project.org/doc/qt-5/qsortfilterproxymodel.html

OTHER TIPS

My suggestion would be to update the actor model on a selectionChanged() signal from the selectionModel of your shot QTableView.

Each time the signal is emitted (when the shot selection is changed), you need to reset the actor model and then iterate over the selected model indexes within your selection, and get a reference to the shot object for each selected row of your model. For each shot object you can get a list of actors. Then you need to check if the actor is in the actor model, and if it is not, add it in.

Now this is a little inefficient because you are resetting the actor model each time the selection of the shot model is changed. You do have information from the selectionChanged() signal about which rows were deselected, so you could instead remove entries from your actor model when rows of the shot model re deselected, but only if no other selected shot row contains a given actor from your deselected shot.

There are a few options for where this code could live in your project, but I would probably but it where you already have access to the views and models for both actors and shots. This is probably in your subclass of QMainWindow, but without seeing code it is hard to tell!

Hope that helps :)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top