Tuesday 4 February 2014

Using the Connections element with HsQML

I was asked recently if the Connections element could be used to declaratively connect QML actions to signals defined in Haskell code. I wasn't completely sure if it would work off-hand so I wrote the following example program with HsQML 0.2.x to find out (Hint: the answer is yes).

To begin with, we need a Haskell program which will load a QML document and fire off some signals. The following program forks off a thread which blocks for the user to enter a new line in the terminal window and fires a signal every time they do. The context object has two members, the signal we're experimenting with and a property called 'self' whose function will become apparent shortly.

{-# LANGUAGE DeriveDataTypeable, TypeFamilies #-}
import Graphics.QML
import Data.Typeable
import Data.Tagged
import Control.Concurrent
import Control.Monad

main :: IO ()
main = do
    ctx <- newObject MainObject
    tid <- forkIO $ forever $ do
        putStrLn "Press ENTER to run animation"
        void $ getLine
        fireSignal (Tagged ctx ::
            Tagged TheSignal (ObjRef MainObject))
    runEngineLoop defaultEngineConfig {
        contextObject = Just $ anyObjRef ctx}
    killThread tid

data TheSignal deriving Typeable
instance SignalKey TheSignal where
    type SignalParams TheSignal = IO ()

data MainObject = MainObject deriving Typeable
instance Object MainObject where
    classDef = defClass [
        defPropertyRO "self" ((\x -> return x) ::
            ObjRef MainObject -> IO (ObjRef MainObject)),
        defSignal (Tagged "theSignal" ::
            Tagged TheSignal String)]

The QML document to accompany the above program follows below. It should be placed in a file called 'main.qml' in order to be loaded by the defaultEngineConfig. You could set the initialURL field to something else if you wanted, but I'm trying to keep the code short.

import Qt 4.7
Rectangle {
    id: root
    width: 500; height: 500
    color: "red"
    Rectangle {
        id: square
        x: 150; y: 150; width: 200; height: 200
        color: "yellow"
        Rectangle {
            width: 50; height: 50; color: "black"
        }
        transform: Rotation {
            id: rotateSquare
            origin.x: 100; origin.y: 100; angle: 0
        }
        NumberAnimation {
            id: rotateAnim
            target: rotateSquare; property: "angle"
            from: 0; to: 360; duration: 1500
        }
        Connections {
            target: self
            onTheSignal: rotateAnim.start()
        }
    }
}

The code for the Connections element is highlighted in bold. Of its two attributes, the first, called 'target', specifies the object with signals that we want to bind handlers to. In this example the signal is a member of the global object and this complicates matters because it's not straightforward to write an expression which yields the global object. Hence, I placed the 'self' property on the global object to provide a convenient way of the getting a reference to it.

There are ways to get the global object, but they're not particularly pretty and I don't fully trust that kind of thing inside Qt's script environment anyway.

The second attribute specifies the signal binding. Specifically, the attribute name identifies the signal and is derived by pre-pending the string 'on' to the actual signal name. Hence, in this case, binding to 'theSignal' is specified using the attribute 'onTheSignal'. The value of the attribute is the JavaScript code to be executed when the signal fires. In our example it causes a simple little animation to occur.

Up to now, the only example I provided of using signals was the hsqml-morris demo application. It's not a great example of idiomatic QML because it uses a big chunk of JavaScript to work around some of the present limitations of HsQML's marshalling facilities (e.g. no lists/arrays). It makes no great attempt to be a "pure" QML application, so it just calls the signal's connect() method to attach it via JavaScript.

You could use the same approach with this test program by replacing the Connections element with the following code snippet:

        Component.onCompleted: {
            self.theSignal.connect(rotateAnim.start);
        }

The 'self' property is superfluous here because we can access the signal member on the global object directly. However, it's a slightly unfair comparison because the JavaScript code only covers connecting to the signal, whereas the Connections element also handles disconnections. When you're dynamically creating and destroying Components using things like the Repeater element, this is important to prevent overloading your signals with handlers that are never cleaned up.

The Connections element also allows the target attribute to be specified with a property or dynamic expression. If the value of the target expression changes at runtime then all the signal handlers will be disconnected and reconnected to the new object.

Addendum: Writing this example has made me think that HsQML relies too heavily on top-level data and instance declarations. I'd like to rectify that in the future by making QML classes first-class values on the Haskell side.

No comments:

Post a Comment