16
votes

I'm going crazy. I have a ListView inside a ScrollView, hooked up to a model that inherits QAbstractListModel. When objects are added to the model, the ListView shows them using a delegate. So far, so good.

But I really want the view to stay scrolled to the bottom (like a chat window), and I'm having a very difficult time making that happen. Here is the relevant QML code:

Rectangle {
    ScrollView {
        [anchor stuff]

        ListView {
            id: messageList
            model: textMessageFiltered
            delegate: messageDelegate
        }
    }

    TextField {
        id: messageEditor
        [anchor stuff]

        onAccepted: {
            controller.sendTextMessage(text)
            text = ""

            /* This works. */
            //messageList.positionViewAtEnd();
        }
    }

    Component {
        id: messageDelegate
        Rectangle {
            anchors.left: parent.left
            anchors.right: parent.right

            color: "white"
            height: nameText.height + 4

            Text {
                id: nameText
                wrapMode: Text.Wrap
                text: "<b>" + authorName + " (" + authorId + ")</b>  " + message
                [anchor stuff]
            }
            ListView.onAdd: {
                console.log("This prints just fine!")
                messageList.positionViewAtEnd()
            }
        }
    }
}

The really strange thing, is that messageList.positionViewAtEnd() (at the end of the file) actually jumps it to the beginning. Without the call, the view stays where it is, even as new entries appear in the list. And indeed, if you look at the Qt documentation for the ListView.positionViewAtEnd(), it says:

Positions the view at the beginning or end, taking into account ...

Is that a silly error in the documentation, or what? I've tried everything I can think of to make this work, particularly the positionViewAtIndex() method and using highlighters to force the scroll to happen. But nothing works. Note the /* This works. */ comment in the source code above. When that is enabled, it works totally fine! (except of course, it jumps to the ListView.count()-2 index, instead of the end of the list)

Does anyone have any idea what might be wrong here? Any examples I could try to prove that there's a terrible, terrible bug in QML?

I'm using Qt 5.3.1 with QtQuick 2.0 (or 2.1 or 2.2 fail too). I've tried many, many other configurations and code as well, so please ask if you need more info. I've completely exhausted my google-fu.

Thanks!


Edit 1

While the accepted answer does solve the above problem, it involves adding the Component.onCompleted to the delegate. This seems to cause problems when you scroll the list, because (I believe) the delegates are added to the view when you scroll up, causing the onCompleted trigger to be called even if the model item isn't new. This is highly undesirable. In fact, the application is freezing when I try to scroll up and then add new elements to the list.

It seems like I need a model.onAdd() signal instead of using the existence of a delegate instance to trigger the scroll. Any ideas?


Edit 2

And how does this NOT work?

    ListView {
        id: messageList
        model: textMessageFiltered
        delegate: messageDelegate

        onCountChanged: {
            console.log("This prints properly.")
            messageList.positionViewAtEnd()
        }
    }

The text "This prints properly" prints, so why doesn't it position? In fact, it appears to reset the position to the top. So I tried positionViewAtBeginning(), but that did the same thing.

I'm totally stumped. It feels like a bug.

2
What's wrong with your commented out solution? That works for me using Qt 5.4 on Ubuntu (it is positioned right at the end from what I can see, not count - 2). Please create a bug report for the hang that occurs when scrolling and onCountChanged not working.Mitch
The problem is that the item isn't added to the list instantly (the actual item is added from a server request), and so either it is off by one (on the sender) or doesn't update at all (on the other client). The positionViewAtBeginning() must act on the list itself being updated. I submitted a bad bug request (QTBUG-41571), so I hope this post is enough to clarify the problem.jmbeck
Have you manage to solve the problem? I have the exact problem and I can't find any solution. Thx!Ispas Claudiu
Unfortunately, no. I ended up working around the problem by redesigning the GUI. I was hoping this was solved in 5.5, but I haven't checked. If you figure it out, come back! :)jmbeck

2 Answers

8
votes

You need to set the currentIndex as well.

testme.qml

import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Window 2.0

ApplicationWindow {
    title: qsTr("Hello World")
    width: 300
    height: 240

    ScrollView {
        anchors.fill: parent

        ListView {
            anchors.fill: parent

            id: messageList
            model: messageModel
            delegate: Text { text: mytextrole }
            highlight: Rectangle { color: "red" }
            highlightMoveDuration: 0

            onCountChanged: {
                var newIndex = count - 1 // last index
                positionViewAtEnd()
                currentIndex = newIndex
            }
        }
    }

    ListModel {
        id: messageModel
        ListElement { mytextrole: "Dog"; }
        ListElement { mytextrole: "Cat"; }
    }

    Timer {
        property int counter: 0
        running: true
        interval: 500
        repeat: true

        onTriggered: {
            messageModel.append({"mytextrole": "Line" + (counter++)})
        }
    }
}

There is still some jumping to the first element and jumping back down for a fraction of a second.

6
votes

There is a note in documentation:

Note: methods should only be called after the Component has completed. To position the view at startup, this method should be called by Component.onCompleted.

Change your ListView.onAdd: to

Component.onCompleted: {
    console.log("This prints just fine!")
    messageList.positionViewAtEnd()
}

And it works well.

In your case, the ListView emits add signal before the new delegate is created and completed. The ListView is still working on something behind the scene, so positionViewAtEnd cannot work as expected. And /* This works. */ because it is called after the new delegate is completed. However, don't assume this always works. Simply follow the note, call positionViewAtEnd in Component.onCompleted, in documentation.