Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Problem keeping ListView highlight in view after model update



  • The problem: When moving the highlight in a ListView, the ListView normally scrolls to keep the highlight in view. However, if the model changes and the highlight is moved while the highlight is not fully visible, as in the example below, the ListView stops following the highlight... and stays that way.

    For background, my actual application displays a list of script steps and you can add, delete, scroll, position and single-step the script steps, creating and debugging a small program if you will. This minimal example displays colored boxes instead of steps, with one red step always the last one. I actually use a C++ model, but QML ListModel demonstrates the problem just as well.

    main.cpp:

    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    int main(int argc, char *argv[]) {
        QGuiApplication app(argc, argv);
        QQmlApplicationEngine engine;
        engine.load("qrc:/main.qml");
        return app.exec();
    }
    

    main.qml:

    import QtQuick 2.12
    import QtQuick.Window 2.12
    import QtQuick.Controls 2.12
    import QtQuick.Layouts 1.12
    import QtQml 2.12
    
    Window {
        width: 400
        height: 600
        visible: true
        title: "Example"
    
        ListModel {
            id: list
            ListElement { red: 1; green: 0; blue: 0 } // Last element, always displayed
        }
    
        ListView {
            id: view
            anchors { top: parent.top; left: parent.left; right: parent.right }
            height: parent.height*9/10
            spacing: 5
            clip: true
            highlightFollowsCurrentItem: true // This is the default, but for emphasis
            highlightMoveDuration: 200
    
            model: list
            currentIndex: 0
    
            highlight: Rectangle {
                z: 3
                color: "transparent"
                border { color: "black"; width: 5 }
                Text {
                    anchors.centerIn: parent
                    text: "current"
                }
            }
    
            delegate: Rectangle {
                width: parent.width
                height: parent.width/5
                color: Qt.rgba(red, green, blue, 1)
                MouseArea {
                    anchors.fill: parent
                    onClicked: view.currentIndex = index;
                }
            }
        }
    
        RowLayout {
            anchors { top: view.bottom; left: parent.left; right: parent.right; bottom: parent.bottom }
            Button {
                text: "add" // Add new step and move down
                onClicked: {
                    list.insert(view.currentIndex, { red: Math.random(), green: Math.random(), blue: Math.random() });
                    ++view.currentIndex;
                }
            }
            Button {
                text: "del" // Delete last added step and move up
                onClicked: {
                    if (view.currentIndex > 0) {
                        list.remove(view.currentIndex-1, 1);
                        --view.currentIndex;
                    }
                }
            }
            Button {
                text: "step" // Step one element down
                onClicked: {
                    if (view.currentIndex < list.count-1) {
                        ++view.currentIndex;
                    }
                }
            }
        }
    }
    

    Press "add" 6 times and the current item starts disappearing outside the visible area. However, scroll down, select a different item than the last, then select the last item again.

    Now observe the desired behavior, (i.e. my desired behavior... which happens after at least one update of "currentIndex" that is not combined with a simultaneous model update). We can now add new items, remove, reposition and step while the current item and highlight is always kept within visible range. It is not actually documented that this behavior should be achievable, I believe, but it sure is nice and just what I want. (When using my C++ model the setting of "currentIndex" happens in a different manner, so I am actually able to get into this desired state from the start.)

    However, if you drag the view so that the current item is not entirely visible, then add or delete an item, the desired behavior derails, the view no longer scrolls to keep the current item in view. It seems that the model update combined with the change of currentIndex, combined with current item not being entirely visible causes it. It stays derailed until an independent update of "currentIndex" occurs. If currentIndex is set by mouse or by the "step" button, the desired behavior starts working again, using the "step" button even with the current item outside view, the view scrolls into position again. It seems that the change of currentIndex without a simultaneous model update makes it work again.

    I have tried all kinds of of things, like "onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)" (surely, that should work!), using "highlightRangeMode" in various ways, and using Timer (with interval: 0) to set the currentIndex or positionViewAtIndex "later". Nothing I have tried works. I have studied the source of qquickitemview.cpp, to see if I can understand what derails and en-rails the desired behavior (my current guess is "trackedItem" being reset). I did not actually debug the Qt source to see what is happening (it seems like a lot of work to set this up).

    I don't really want to fork and fix the Qt code to make it work, even though this looks like a bug, I would prefer to make this work with Qt as it is. Surely, by wriggling around with the code long enough it should be possible.


Log in to reply