QML state changes on "when" clause sets default state

Viewed 1564

When using the when clause of a QML state, it sometimes goes through the default state ("") before going to the final state:

import QtQuick 2.14
import QtQuick.Controls 2.14

Button {
    id: root

    width: 30
    height: 30

    states: [
        State {
            name: "PRESSED"
            when: pressed
        },
        State {
            name: "NOTPRESSED"
            when: !pressed
        }
    ]

    onStateChanged: {
        console.log("State: " + state);
    }
}

pressed here is a boolean, so whatever its value, onStateChanged() should be triggered with one of the states I defined. But here, the output is:

$ qmlscene ./Button.qml
qml: State: NOTPRESSED
qml: State: 
qml: State: PRESSED
qml: State: NOTPRESSED
qml: State: 
qml: State: PRESSED
qml: State: NOTPRESSED
qml: State: 
qml: State: PRESSED
qml: State: NOTPRESSED

Why does Qt changes the state from NOTPRESSED to "" before setting the target state PRESSED?

I've tested this component with Qt 5.14.1 and 5.15.

2 Answers

EDIT: it's a QT Bug https://bugreports.qt.io/browse/QTBUG-86695

--

The problem with the 'when' clause enabling the default unnamed state "" in some situations, even if it does not appear in the list of is not specific to Button. If you have more than one state and transitions between states, you usually can't use the when clause anymore until this bug is fixed.

If you have only two stated you may avoid the unnamed "" default state as follows:

  1. put the default state at the end of the state array and
  2. specify when: 1 == 1 (if you do not need to support QT 5.12 you may also write when: true here).

To see how this workaround fixes the OPs problem consider the following example (deliberately not using Button to also show that the problem is much more general and is not related to the released state of Button):

Item {
    id: item
    anchors.fill: parent

    property bool toggle

    Button {
        anchors.fill: parent
        onClicked: item.toggle = ! item.toggle
    }

    states: [
        State {
            name: "true"
            when: item.toggle
        },
        State {
            name: "false"
            when: ! item.toggle
        }
    ]

    onStateChanged: {
        console.log("state:", state, "toggle:", toggle)
    }
}

In QT 5.12.9 clicking twice produces the following output (first line on startup)

qml: state: false toggle: false
qml: state: true toggle: true
qml: state: false toggle: false

In QT 5.15.2 clicking twice produces the following output (first line on startup)

qml: state: false toggle: false
qml: state:  toggle: true
qml: state: true toggle: true
qml: state: false toggle: false

So switching form toggle: false to toggle: true goes through the "" default state in QT 5.15.2 rather than directly to the "true" state. If you change the sequence of state definitions, the other state change goes through "". It looks like there is a moment when the when clause of the "false" state is false, but the when clause of the "true" state is not true yet.

In QT 5.15.2 specifying when: true on the "false" state works, i.e. the states toggle as in QT 5.12.9 and the default unnamed "" state is never applied. But this workaround produces an error (Invalid property assignment: unsupported type "QQmlBinding*") in QT 5.12.9 (which I also need to support). This error is avoided by when: 1 == 1. So here is my final workaround:

Item {
    id: item
    anchors.fill: parent

    property bool toggle

    Button {
        anchors.fill: parent
        onClicked: item.toggle = ! item.toggle
    }

    states: [
        State {
            name: "true"
            when: item.toggle
        },
        State {
            name: "false"
            when: 1 == 1
        }
    ]

    onStateChanged: {
        console.log("state:", state, "toggle:", toggle)
    }
}

State "" means released. If you put in your code this:

State {
    name: "released"
    when: root.released
}

Full code:

Button {
    id: root
    autoRepeat: true

    anchors.fill: parent

    states: [
        State {
            name: "PRESSED"
            when: root.pressed
        },
        State {
            name: "NOTPRESSED"
            when: !root.pressed
        },
        State {
            name: "released"
            when: root.released
        }
    ]

    onStateChanged: {
        console.log("State: " + state);
    } 
}

You will see, that "" will be changed to released.

My output, If someone need:

qml: State: NOTPRESSED
qml: State: released
qml: State: PRESSED
Related