Search code examples
pythonautocompletepyqtqsortfilterproxymodelqcompleter

PyQt QSortFilterProxyModel index from wrong model passed to mapToSource?


I want to get the integer stored in [(1, 'cb'), (3, 'cd'), (7, 'ca'), (11, 'aa'), (22, 'bd')] when I select the drop down auto complete item.

Because I used a QSortFilterProxyModel, when using down key to select the item, the index is from the proxy model.

I read in the documentation that I should use mapToSource to get the index in original model, but here I got an error message index from wrong model passed to mapToSource and the index.row() is always -1. What am I missing? Thanks!

The error is:

row in proxy model 0
QSortFilterProxyModel: index from wrong model passed to mapToSource 
row in original model -1

code:

from PyQt4.QtCore import *
from PyQt4.QtGui import *


import sys
import re
import signal
signal.signal(signal.SIGINT, signal.SIG_DFL)


class MyModel(QStandardItemModel):

    def __init__(self, parent=None):
        super(MyModel, self).__init__(parent)

    def data(self, index, role):

        symbol = self.symbol_data[index.row()]
        if role == Qt.DisplayRole:
            return symbol[1]

        elif role == Qt.UserRole:
            return symbol[0]

    def setup(self, data):
        self.symbol_data = data
        for line, name in data:
            item = QStandardItem(name)
            self.appendRow(item)


class MyGui(QDialog):

    def __init__(self, parent=None):

        super(MyGui, self).__init__(parent)

        symbols = [(1, 'cb'), (3, 'cd'), (7, 'ca'), (11, 'aa'), (22, 'bd')]

        model = MyModel()
        model.setup(symbols)

        layout = QVBoxLayout(self)
        self.line = QLineEdit(self)

        layout.addWidget(self.line)

        self.setLayout(layout)

        completer = CustomQCompleter()

        completer.setModel(model)
        completer.setCaseSensitivity(Qt.CaseInsensitive)
        completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
        completer.setWrapAround(False)

        self.line.setCompleter(completer)

        self.completer = completer

        self.completer.highlighted[QModelIndex].connect(self.test)

        # qApp.processEvents()
        # QTimer.singleShot(0, self.completer.complete)
        self.line.textChanged[QString].connect(self.pop)

    def pop(self, *x):
        text = x[0]
        self.completer.splitPath(text)
        QTimer.singleShot(0, self.completer.complete)

        self.line.setFocus()

    def test(self, index):
        print 'row in proxy model', index.row()
        print 'row in original model', self.completer.model().mapToSource(index).row()
        # print 'line in original model:',
        # self.completer.model().sourceModel().symbol_data[x[0].row()][0]


class CustomQCompleter(QCompleter):

    def __init__(self, parent=None):
        super(CustomQCompleter, self).__init__(parent)
        self.local_completion_prefix = ""
        self.source_model = None
        self.first_down = True

    def setModel(self, model):
        self.source_model = model
        self._proxy = QSortFilterProxyModel(
            self, filterCaseSensitivity=Qt.CaseInsensitive)
        self._proxy.setSourceModel(model)
        super(CustomQCompleter, self).setModel(self._proxy)

    def splitPath(self, path):
        self.local_completion_prefix = str(path)
        self._proxy.setFilterFixedString(path)
        return ""

    def eventFilter(self,  obj,  event):

        if event.type() == QEvent.KeyPress:
            'This is used to mute the connection to clear lineedit'
            if event.key() in (Qt.Key_Down, Qt.Key_Up):
                curIndex = self.popup().currentIndex()

                if event.key() == Qt.Key_Down:
                    if curIndex.row() == self._proxy.rowCount()-1:
                        print 'already last row', curIndex.row()
                        if self._proxy.rowCount() == 1:
                            pass
                        else:
                            return True
                else:
                    if curIndex.row() == 0:
                        print 'already first row'
                        return True

                if curIndex.row() == 0 and self.first_down:
                    print 'already row 0 first'
                    self.popup().setCurrentIndex(curIndex)
                    self.first_down = False
                    return True

        super(CustomQCompleter, self).eventFilter(obj,  event)
        return False


if __name__ == '__main__':

    app = QApplication(sys.argv)
    gui = MyGui()
    gui.show()
    sys.exit(app.exec_())

update: This is resolved, Thanks for the help from Avaris in #pyqt. It turns out that I can do this to map the index to original model

proxy_index= self.completer.completionModel().mapToSource(index)
print 'original row:', self.completer.model().mapToSource(proxy_index).row()

or even better:

print 'data:', index.data(Qt.UserRole).toPyObject()

becuase: " completionModel() is actually a proxy model on .model()

you don't need to mess with mapToSource for that. index.data(Qt.UserRole) should give you that number regardless of which index is returned

just an fyi, you rarely need to use mapToSource outside of a (proxy) model. it's mostly for internal use. a proper proxy should forward all relevant queries from the source. so you can use the proxy as if you're using the source one -Avaris "


Solution

  • paste the correct code here for reference

    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    
    
    import sys
    import re
    import signal
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    
    
    class MyModel(QStandardItemModel):
    
        def __init__(self, parent=None):
            super(MyModel, self).__init__(parent)
    
        def data(self, index, role):
    
            symbol = self.symbol_data[index.row()]
            if role == Qt.DisplayRole:
                return symbol[1]
    
            elif role == Qt.UserRole:
                return symbol[0]
    
        def setup(self, data):
            self.symbol_data = data
            for line, name in data:
                item = QStandardItem(name)
                self.appendRow(item)
    
    
    class MyGui(QDialog):
    
        def __init__(self, parent=None):
    
            super(MyGui, self).__init__(parent)
    
            symbols = [(1, 'cb'), (3, 'cd'), (7, 'ca'), (11, 'aa'), (22, 'bd')]
    
            model = MyModel()
            model.setup(symbols)
    
            layout = QVBoxLayout(self)
            self.line = QLineEdit(self)
    
            layout.addWidget(self.line)
    
            self.setLayout(layout)
    
            completer = CustomQCompleter()
    
            completer.setModel(model)
            completer.setCaseSensitivity(Qt.CaseInsensitive)
            completer.setCompletionMode(QCompleter.UnfilteredPopupCompletion)
            completer.setWrapAround(False)
    
            self.line.setCompleter(completer)
    
            self.completer = completer
    
            self.completer.highlighted[QModelIndex].connect(self.test)
    
            # QTimer.singleShot(0, self.completer.complete)
            self.line.textChanged[QString].connect(self.pop)
    
        def pop(self, *x):
            text = x[0]
            self.completer.splitPath(text)
            QTimer.singleShot(0, self.completer.complete)
    
            self.line.setFocus()
    
        def test(self, index):
            print 'row in completion model', index.row()
            print 'data:', index.data(Qt.UserRole).toPyObject()
    
    class CustomQCompleter(QCompleter):
    
        def __init__(self, parent=None):
            super(CustomQCompleter, self).__init__(parent)
            self.local_completion_prefix = ""
            self.source_model = None
            self.first_down = True
    
        def setModel(self, model):
            self.source_model = model
            self._proxy = QSortFilterProxyModel(
                self, filterCaseSensitivity=Qt.CaseInsensitive)
            self._proxy.setSourceModel(model)
            super(CustomQCompleter, self).setModel(self._proxy)
    
        def splitPath(self, path):
            self.local_completion_prefix = str(path)
            self._proxy.setFilterFixedString(path)
            return ""
    
        def eventFilter(self,  obj,  event):
    
            if event.type() == QEvent.KeyPress:
                'This is used to mute the connection to clear lineedit'
                if event.key() in (Qt.Key_Down, Qt.Key_Up):
                    curIndex = self.popup().currentIndex()
    
                    if event.key() == Qt.Key_Down:
                        if curIndex.row() == self._proxy.rowCount()-1:
                            print 'already last row', curIndex.row()
                            if self._proxy.rowCount() == 1:
                                pass
                            else:
                                return True
                    else:
                        if curIndex.row() == 0:
                            print 'already first row'
                            return True
    
                    if curIndex.row() == 0 and self.first_down:
                        print 'already row 0 first'
                        self.popup().setCurrentIndex(curIndex)
                        self.first_down = False
                        return True
    
            super(CustomQCompleter, self).eventFilter(obj,  event)
            return False
    
    
    if __name__ == '__main__':
    
        app = QApplication(sys.argv)
        gui = MyGui()
        gui.show()
        sys.exit(app.exec_())