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.