Search code examples
pythonpysideqlistviewqsortfilterproxymodelqabstractlistmodel

Get selected item from filtered QListView


When I run my tool and double-click and item it prints the name to the console, in this case, it prints 'comments'. However if I type in the search bar, which filters out the list and then double-click that image again, it returns me the wrong name. I'm not sure where I'm going wrong here. Below is the code, for the entire app.

enter image description here

To test just change the folder path at the end of the code to a local folder on your computer containing some jpg. I find that it's a very simple application with a just a search filter on the listview. I'm confused on why it would return on the wrong item. I'm guessing it has to do with how I retrieve the selection.

On my QAbstractListModel I have a method which I pass a selection

def getSelectedItems(self, selection):
    objs = []
    for i, index in enumerate(selection):
        item = self.getItem(index)
        objs.append(item)
    return objs

FULL CODE

import sys
import os
from PySide import QtGui, QtCore
from PySide import QtGui as QtWidgets


class AssetItem(object):
    def __init__(self, filepath):
        self._name = ''
        self._filepath = ''
        self.filepath = filepath

    @property
    def filepath(self):
        return self._filepath

    @filepath.setter
    def filepath(self, value):
        self._filepath = value
        self._name, self._extension = os.path.splitext(os.path.basename(self.filepath))

    @property
    def name(self):
        return self._name


class AssetModel(QtCore.QAbstractListModel):
    NameRole = QtCore.Qt.UserRole + 1

    def __init__(self, *args, **kwargs):
        QtCore.QAbstractListModel.__init__(self, *args, **kwargs)
        self._items = []

    def rowCount(self, index=QtCore.QModelIndex()):
        return len(self._items)

    def addItem(self, assetItem):
        self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
        self._items.append(assetItem)
        self.endInsertRows()

    def getItem(self, index):
        row = index.row()
        if index.isValid() and 0 <= row < self.rowCount():
            return self._items[row]

    def getSelectedItems(self, selection):
        objs = []
        for i, index in enumerate(selection):
            item = self.getItem(index)
            objs.append(item)
        return objs

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return None
        if 0 <= index.row() < self.rowCount():
            item = self._items[index.row()]
            if role == AssetModel.NameRole:
                return item.name
            elif role == QtCore.Qt.TextAlignmentRole:
                return QtCore.Qt.AlignCenter
            elif role == QtCore.Qt.ToolTipRole:
                return item.name
            elif role == QtCore.Qt.DisplayRole:
                return item.name


class SortedModel(QtGui.QSortFilterProxyModel):
    def __init__(self, *args, **kwargs):
        super(SortedModel, self).__init__(*args, **kwargs)
        self._patterns = {}

    def set_pattern(self, role, value):
        self._patterns[role] = value

    def filterAcceptsRow(self, sourceRow, sourceParent):
        sm = self.sourceModel()
        ix = sm.index(sourceRow)
        if ix.isValid():
            val = True
            for role, fvalue in self._patterns.items():
                value = ix.data(role)
                val = val and self.filter(value, fvalue, role)
            return val
        return False

    @staticmethod
    def filter(value, fvalue, role):
        '''
        fvalue: search value
        value: properties value being tested
        '''
        if role == AssetModel.NameRole:
            if fvalue == []:
                return True
            else:
                # change all to any for expanded search
                return all(any(x in y for y in value) for x in fvalue)
        else:
            return False


class ShopWidget(QtWidgets.QWidget):
    def __init__(self,parent=None, path=None):
        super(ShopWidget, self).__init__(parent)
        self.TITLE = 'Shop'
        self.VERSION = '1.0.0' # MAJOR.MINOR.PATCH
        self.setWindowTitle(self.TITLE + ' | ' + self.VERSION)
        self.resize(1000,700)

        # properties
        self.path = path

        # controls
        self.ui_search_bar = QtWidgets.QLineEdit()
        self.ui_search_bar.setPlaceholderText('Search...')

        self.ui_asset_list = QtWidgets.QListView()
        self.ui_asset_list.setViewMode(QtWidgets.QListView.IconMode)
        self.ui_asset_list.setResizeMode(QtWidgets.QListView.Adjust)
        self.ui_asset_list.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.ui_asset_list.setIconSize(QtCore.QSize(256, 256))
        self.ui_asset_list.setMovement(QtWidgets.QListView.Static)
        self.ui_asset_list.setSpacing(10)
        self.ui_asset_list.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.ui_asset_selection_model = self.ui_asset_list.selectionModel()
        self.ui_asset_list.setIconSize(QtCore.QSize(128,128))

        self.sorted_model = SortedModel()
        self.sorted_model.setSourceModel(AssetModel())
        self.ui_asset_list.setModel(self.sorted_model)

        # layout
        search_layout = QtWidgets.QHBoxLayout()
        search_layout.addWidget(self.ui_search_bar)

        main_layout = QtWidgets.QVBoxLayout()
        main_layout.addLayout(search_layout)
        main_layout.addWidget(self.ui_asset_list)

        self.setLayout(main_layout)

        # connections
        self.ui_search_bar.textChanged.connect(self.search_value_changed)
        self.ui_search_bar.keyPressEvent = self.search_bar_key_event
        self.ui_asset_list.doubleClicked.connect(self.on_item_double_clicked)

        # constructor
        self.path = path
        self.populate_asset_list()


    # methods
    def populate_asset_list(self):
        extensions = ['.jpg']
        directory = self.path

        if not os.path.isdir(directory):
            return

        for root, subdirs, files in os.walk(directory):
            for f in files:
                filepath = os.path.join(root, f)
                if not os.path.isfile(filepath):
                    continue
                if not os.path.splitext(filepath)[-1] in extensions:
                    continue
                self.ui_asset_list.model().sourceModel().addItem(AssetItem(filepath))


    def search_bar_key_event(self, event):
        if event.key() == QtCore.Qt.Key_Escape:
            self.ui_search_bar.clear()
        QtWidgets.QLineEdit.keyPressEvent(self.ui_search_bar, event)


    def search_value_changed(self, text):
        filters = filter(None, text.lower().split(' '))
        model = self.ui_asset_list.model()
        model.set_pattern(AssetModel.NameRole, filters)
        model.invalidateFilter()


    def import_assets(self):
        selection = self.ui_asset_list.selectionModel().selectedRows()
        assets = self.ui_asset_list.model().sourceModel().getSelectedItems(selection)

        for x in assets:
            print x.name


    # actions
    def on_item_double_clicked(self, index):
        self.import_assets()


# Main
# -----------------------------------------------------------------------------
def main():
    app = QtWidgets.QApplication(sys.argv)
    ex = ShopWidget(path='C:/Users/jmartini/Desktop/Temp/images')
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

Solution

  • When selecting the QModelIndex with the method selectionModel().selectedRows() this returns the indexes with respect to the model that was established to the view, in your case the SortedModel, but if you want to obtain an item you must have a QModelIndex that belong to AssetModel, that is, to the source model, for this you must use mapToSource of the proxy model:

    def import_assets(self):
        selection = self.ui_asset_list.selectionModel().selectedRows()
        selection_x = [self.ui_asset_list.model().mapToSource(index) for index in selection]
        assets = self.ui_asset_list.model().sourceModel().getSelectedItems(selection_x)
    
        for x in assets:
            print(x.name)