I'm trying to create a QML based application that performs an action at intervals. It seemed that states would be the ideal way you do this but in fact the QML meaning of the word state does not seem to accord with that used in the finite state machine literature. In particular the when property doesn't cause the state to be entered at the moment that the when expression becomes true, instead it means that the state is valid only while the expression is true. This means that if the expression becomes false then the UI exits from that state and enters whatever other state has a true when expression, if there are none then it enters the default state.

In an attempt to avoid this I decided to change from state to state explicitly by assigning values to the state property. This works a little better but one odd behaviour remained unchanged and that is that the text of a button suddenly changed to its default value even though my code had not, or so I thought, caused a state change.

At first I thought this might be a peculiarity of the Harmattan Button element so I recreated just enough of the UI in pure QML to exercise the code and found that the problem still exists.

The code is two files. The main file is qml-test.qml which contains the state definitions and uses an element defined in Controls.qml. There is a single button implemented as a Rectangle, Text, and MouseArea. The states are IDLE, INITIAL_DELAY, and RUNNING. Here is the console log:
Code:
Starting /home/whitefoot/QtSDK/Desktop/Qt/4.8.1/gcc/bin/qmlviewer -I /home/whitefoot/QtSDK/Desktop/Qt/4.8.1/gcc/imports /home/whitefoot/qt-projects/qml-test/qml-test.qml
Qml debugging is enabled. Only use this in a safe environment!
Text changed to: Start
state: IDLE
become checked
change state to initial
Text changed to: Stop
Text changed to: Start
Text changed to: Stop
state: INITIAL_DELAY
Text changed to: 
Text changed to: Stop
state: RUNNING
Text changed to: 
Do timed action
Do timed action
Do timed action
Do timed action
Do timed action
Do timed action
become unchecked
change state to IDLE
Text changed to: Start
state: IDLE
become checked
change state to initial
Text changed to: Stop
Text changed to: Start
Text changed to: Stop
state: INITIAL_DELAY
Text changed to: 
Text changed to: Stop
state: RUNNING
Text changed to: 
Do timed action
Do timed action
Do timed action
become unchecked
change state to IDLE
Text changed to: Start
state: IDLE
/home/whitefoot/QtSDK/Desktop/Qt/4.8.1/gcc/bin/qmlviewer exited with code 0
Notice the lines beginning "Text changed to:". The only explicit changes are to "Start" and "Stop". Everything works as expected:
  • Click start: timer begins, enter INITIAL_DELAY state, change button text to "Stop"
  • Timer times out after 3 seconds, enters RUNNING state, executes timed action
  • Timer times out after 1 second, executes timed action
  • ...
  • Click button, enter IDLE state, timer stops, button text updated to "Start"


except that the button text is changed to an empty string, just after the state changes from INITIAL_DELAY to RUNNING. This empty string isn't even the default value. If this were merely a momentary change it might be acceptable but it persists until the next explicit state change.

I'm new to QML so I'm quite ready to believe that I have missed something obvious.

It seems that there are ways around this using C++ but really I'd like to avoid that if I can as I haven't used it in so many years. See http://blog.codeimproved.net/tag/qml/.

Can anyone suggest a fix, workaround, correction, or clarification?

Here is the code:
Code:
// qml-test.qml
import QtQuick 1.1


Rectangle {
    width: 360
    height: 360
    state: "IDLE"

    states: [
        State {
            name: "IDLE"
            PropertyChanges {
                target: controls;
                timer.running: false
                startButton.text: "Start"
            }
            StateChangeScript {
                name: "idle"
                script: {
                    console.log("state: " + state.toString())
                }
            }
        },
        State {
            name: "INITIAL_DELAY"
            PropertyChanges {
                target: controls;
                startButton.text: "Stop"
                timer.interval: controls.initialDelay
                timer.running: true
            }
            StateChangeScript {
                name: "initial_delay"
                script: {
                    console.log("state: " + state.toString())
                }
            }
        },
        State {
            name: "RUNNING";
            PropertyChanges {
                target: controls;
                timer.interval: controls.interval;
                timer.repeat: true;
                timer.running: true
            }
            StateChangeScript {
                name: "running";
                script: {
                  console.log("state: " + state.toString())
                }
            }
        }
    ]

    Controls{
        id: controls
        x:0
        y:0
        onTriggered: {
            if (parent.state != "RUNNING"){
                parent.state = "RUNNING"
            }
            console.log("Do timed action")
        }
        onStart: {
            console.log("change state to initial")
            parent.state = "INITIAL_DELAY"
        }
        onStop: {
            console.log("change state to IDLE")
            parent.state = "IDLE"
        }
    }
}

//Controls.qml
import QtQuick 1.1


Column{
    //width: 100
    //height: 62
    id: controls
    property int initialDelay: 3000
    property int interval: 1000
    property alias startButton: startButton
    property alias timer: timer
    //property bool tick: false;

    signal start();
    signal triggered();
    signal stop();


    Timer{
        id: timer
        //interval: 3000
        repeat: true
        triggeredOnStart: false
        onTriggered: {
            //tick = true;
            parent.triggered()
        }
    }

    Rectangle {
        id: startButton
        property bool checked: false
        property string text
        onTextChanged: {
            buttonText.text = text
        }

        border.width: 1
        border.color:"black"
        visible: true
        color: "white"
        width: 80
        height: 20


        Text {
            id: buttonText
            anchors.fill: parent
            text: "default"
            onTextChanged: {
                console.log("Text changed to: " + text)
            }
        }

        MouseArea{
            anchors.fill: parent

            onClicked: {
                if (parent.checked) {
                    console.log("become unchecked");
                    parent.checked = false
                    parent.color = "white"
                    buttonText.color = "black"
                    controls.stop();
                }else{
                    console.log("become checked");
                    parent.checked = true
                    parent.color = "blue"
                    buttonText.color = "white"
                    controls.start();
                }
            }

        }
    }
}