C

Handling input

This topic explains how to integrate system inputs with Qt Quick Ultralite.

Overview

Qt Quick Ultralite supports touch events and key events by default. These events are handled by the following functions defined in the platforminterface/platforminterface.h header file.

Note: Calling any of the above functions directly, from ISR for example, may corrupt the Qt Quick Ultralite application state. Instead, Qul::EventQueue should be used. You can also use your own queue implementation, but in that case the call to any of the above functions must be in Qt Quick Ultralite context, meaning that it should probably be called from Qul::Platform::exec().

For single touch systems, use the Qul::Platform::SinglePointTouchEventDispatcher helper class. It offers a more convinient way to send touch events to the Qt Quick Ultralite engine, than Qul::PlatformInterface::handleTouchEvent(). With the latter one, you need to handle the multi-touch aspects yourself.

Besides the default touch and key events, it is also possible to send custom events using the Qul::EventQueue class. This requires a custom C++ to QML interface in the application to receive custom events.

Implementing input handling

Typically, the system receives touch and key events via interrupts. To integrate system inputs with Qt Quick Ultralite, set up ISRs to serve the interrupts received from input devices of the system. The ISR then interprets input data, converts it to appropriate format, and sends it to Qt Quick Ultralite engine for further processing.

Sending touch events using dispatcher

Here is an example for single touch system using the Qul::Platform::SinglePointTouchEventDispatcher helper class mentioned above. For clarity, the platform-specific interrupt configuration and functions to get touch event data are not shown here.

First, create a class with Qul::EventQueue for single point touch events and Qul::Platform::SinglePointTouchEventDispatcher as private member. Events are dispatched from reimplemented Qul::EventQueue::onEvent(). Also, a static instance of the event queue is created.

class SinglePointTouchEventQueue : public Qul::EventQueue<Qul::Platform::SinglePointTouchEvent>
{
public:
    void onEvent(const Qul::Platform::SinglePointTouchEvent &event) override { touchEventDispatcher.dispatch(event); }

private:
    Qul::Platform::SinglePointTouchEventDispatcher touchEventDispatcher;
};

static SinglePointTouchEventQueue touchEventQueue;

Next, implement getTouchEvent() helper function to convert platform specific touch data into Qul::Platform::SinglePointTouchEvent.

Qul::Platform::SinglePointTouchEvent getTouchEvent()
{
    static Qul::Platform::SinglePointTouchEvent event;
    int x = 0, y = 0;
    bool pressed = false;

    // Here would be platform specific code to fetch touch data and store it to above x, y and
    // pressed variables or directly to the event variable.

    event.x = x;
    event.y = y;
    event.pressed = pressed;
    event.timestamp = Qul::Platform::currentTimestamp();

    return event;
}

Note: Use Qul::Platform::currentTimestamp() to ensure that input event timestamps are from the same clock as the rest of the Qt Quick Ultralite engine.

Finally, implement touchISR() function to handle interrupts from touch controller and post events to the queue.

void touchISR()
{
    touchEventQueue.postEvent(getTouchEvent());
}

Sending touch events using an event queue

Touch events can be sent to the Qt Quick Ultralite engine using Qul::PlatformInterface::handleTouchEvent() with Qul::EventQueue.

Here is an example.

class TouchPointEventQueue : public Qul::EventQueue<Qul::PlatformInterface::TouchPoint>
{
public:
    void onEvent(const Qul::PlatformInterface::TouchPoint &point) override
    {
        Qul::PlatformInterface::handleTouchEvent(nullptr, /* Primary screen will be used */
                                                 Qul::Platform::currentTimestamp(),
                                                 &point,
                                                 1); /* only one touch point */
    }
};

static TouchPointEventQueue touchPointQueue;

void touchEventISR()
{
    static Qul::PlatformInterface::TouchPoint point;

    // Here would be code to fetch platform specific touch data and store it into point variable.
    // To keep the example simple we just send constant data.
    point.id = 0;
    point.state = Qul::PlatformInterface::TouchPoint::Stationary;
    point.positionX = 100;
    point.positionY = 100;
    point.areaX = 1.f;
    point.areaY = 1.f;
    point.pressure = 0.1f;
    point.rotation = 1.f;
    touchPointQueue.postEvent(point);
}

Sending multi-touch events with multiple screens

Although Qt Quick Ultralite does not support multi-touch or multiple screens yet, the following example still demonstrates how to do it.

// These are just dummy handles for the example
Qul::PlatformInterface::Screen primaryScreenHandle;
Qul::PlatformInterface::Screen secondaryScreenHandle;

// multi-touch with 2 touch points
const int numTouchPoints = 2;
struct TouchPoints
{
    Qul::PlatformInterface::TouchPoint points[numTouchPoints];
};

class PlatformTouchEventQueue : public Qul::EventQueue<TouchPoints>
{
public:
    PlatformTouchEventQueue(Qul::PlatformInterface::Screen *s)
        : screen(s)
    {}

    void onEvent(const TouchPoints &p) override
    {
        Qul::PlatformInterface::handleTouchEvent(screen, Qul::Platform::currentTimestamp(), p.points, numTouchPoints);
    }

private:
    Qul::PlatformInterface::Screen *screen;
};

static PlatformTouchEventQueue primaryScreenTouchEventQueue(&primaryScreenHandle);
static PlatformTouchEventQueue secondaryScreenTouchEventQueue(&secondaryScreenHandle);

void primaryTouchISR()
{
    static TouchPoints p;

    // Here would be platform specific code to fetch touch data for primary screen. To keep the
    // example simple we just send constant data.

    for (int i = 0; i < numTouchPoints; i++) {
        p.points[i].id = i;
        p.points[i].state = Qul::PlatformInterface::TouchPoint::Stationary;
        p.points[i].positionX = 100;
        p.points[i].positionY = 100;
        p.points[i].areaX = 1.f;
        p.points[i].areaY = 1.f;
        p.points[i].pressure = 0.1f;
        p.points[i].rotation = 1.f;
    }
    primaryScreenTouchEventQueue.postEvent(p);
}

void secondaryTouchISR()
{
    static TouchPoints p;

    // Here would be platform specific code to fetch touch data for secondary screen. To keep the
    // example simple we just send constant data.

    for (int i = 0; i < numTouchPoints; i++) {
        p.points[i].id = i;
        p.points[i].state = Qul::PlatformInterface::TouchPoint::Stationary;
        p.points[i].positionX = 150;
        p.points[i].positionY = 150;
        p.points[i].areaX = 0.5f;
        p.points[i].areaY = 0.5f;
        p.points[i].pressure = 0.5f;
        p.points[i].rotation = 2.f;
    }
    secondaryScreenTouchEventQueue.postEvent(p);
}

Handling key input

If the system has keyboard, you can send key events to the application using Qul::PlatformInterface::handleKeyEvent() with Qul::EventQueue. The Qt Quick Ultralite engine passes key events to the application for further prosessing, without interpreting them. Any visual primitive in the application can receive key events, see Keys QML Type for more information.

Here is an example that sends key press event:

struct KeyboardEvent
{
    uint64_t timestamp;
    Qul::PlatformInterface::KeyEventType type;
    int key;
    unsigned int nativeScanCode;
    unsigned int modifiers;
    char *textUTF;
    bool autorepeat;
};

class PlatformKeyboardEventQueue : public Qul::EventQueue<KeyboardEvent>
{
    void onEvent(const KeyboardEvent &event) override
    {
        Qul::PlatformInterface::handleKeyEvent(event.timestamp,
                                               event.type,
                                               event.key,
                                               event.nativeScanCode,
                                               event.modifiers,
                                               event.textUTF,
                                               event.autorepeat);
    }
};

static PlatformKeyboardEventQueue keyboardEventQueue;

void keyboardISR()
{
    // Here would be platform specific code to fetch data from the keyboard.
    static KeyboardEvent event;
    event.timestamp = Qul::Platform::currentTimestamp();
    event.type = Qul::PlatformInterface::KeyPressEvent;
    event.key = Qul::PlatformInterface::Key_A;
    event.nativeScanCode = 0;
    event.modifiers = Qul::PlatformInterface::NoKeyboardModifier;
    event.textUTF = (char *) "A"; /* UTF-8 representation */
    event.autorepeat = false;

    keyboardEventQueue.postEvent(event);
}

Note: Although Qt Quick Ultralite forwards only the key event type, key code, and native scan code to the application, the example shows additional arguments which are not forwarded in current version 1.5 of Qt Quick Ultralite.

Custom input handling

Besides touch and key input events, the system might have other types of input that must be handled by the application. For example, a temperature sensor or rotary knob that lets you provide input to the system. The following sections demonstrates how to handle input for those types.

Temperature sensor input

First, introduce a C++ to QML interface with Qul::EventQueue to send temperature readings from platform to application. The interface is a Qul::Singleton and can be accessed using the instance() function. In addition, define the following properties and methods:

  • temperatureChanged - Qul::Signal to receive temperature readings.
  • Unit - enumeration for different temperature units
  • Qul::Property - to track unit changes.
  • setUnit() - Setter method to set new temperature unit and convert current value to the new unit.
  • onEvent() - Reimplemented Qul::EventQueue::onEvent() method to notify about temperature readings using the temperatureChanged signal.

As the iterface defined is a Qul::Singleton, external construction and copying is prevented. The header containting the C++ to QML interface class must be introduced to QML using the qul_target_generate_interfaces Cmake command in the application target.

struct TemperatureInput : public Qul::Singleton<TemperatureInput>, public Qul::EventQueue<double>
{
    Qul::Signal<void(double value)> temperatureChanged;

    enum Unit { Celsius, Fahrenheit, Kelvin };
    Qul::Property<TemperatureInput::Unit> unit;
    void setUnit(TemperatureInput::Unit newUnit)
    {
        switch (unit.value()) {
        case Celsius:
            if (newUnit == Celsius)
                return;
            newUnit == Kelvin ? cachedValue += 273.15 : cachedValue = cachedValue * 1.8 + 32.0;
            break;
        case Fahrenheit:
            if (newUnit == Fahrenheit)
                return;
            newUnit == Kelvin ? cachedValue = (cachedValue - 32.0) / 1.8 + 273.15
                              : cachedValue = (cachedValue - 32.0) / 1.8;
            break;
        case Kelvin:
            if (newUnit == Kelvin)
                return;
            newUnit == Celsius ? cachedValue -= 273.15 : cachedValue = (cachedValue - 273.15) * 1.8 + 32.0;
            break;
        }
        unit.setValue(newUnit);
        temperatureChanged(cachedValue);
    }

    void onEvent(const double &temperature) override
    {
        temperatureChanged(temperature);
        cachedValue = temperature;
    }

private:
    // Create friendship for a base class to be able to access private constructor
    friend class Qul::Singleton<TemperatureInput>;
    TemperatureInput() {}
    TemperatureInput(const TemperatureInput &);
    TemperatureInput &operator=(const TemperatureInput &);

    double cachedValue;
};

Now that you are ready to send temperature events, post the events via the TemperatureInput singleton instance. The events are posted from the temperatureSensorISR() function, which is a platform-specific interrupt handler for the temperature sensor. Notice how the unit Qul::Property is used here to send readings in the unit that is requested by the application. This is handy if the sensor can provide temperature readings in multiple units to avoid extra conversion.

void temperatureSensorISR()
{
    // Here would be platform specific code to read temperature sensor value.

    switch (TemperatureInput::instance().unit.value()) {
    case TemperatureInput::Celsius:
        TemperatureInput::instance().postEvent(30.0);
        break;
    case TemperatureInput::Fahrenheit:
        TemperatureInput::instance().postEvent(86.0);
        break;
    case TemperatureInput::Kelvin:
        TemperatureInput::instance().postEvent(303.15);
        break;
    default:
        assert(!"Unknown temperature unit requested!");
    }
}

Here is how a simple temperature reader QML application could look like:

import QtQuick 2.15

Rectangle {
    color: "#41CD52"

    Text {
        id: temperature
        anchors.centerIn: parent
        font.pixelSize: 45
        text: "0"

        Text {
            id: temperatureUnit
            property TemperatureInput.Unit unit: TemperatureInput.unit
            anchors.left: temperature.right
            anchors.top: temperature.top
            font.pixelSize: 20

            onUnitChanged: {
                if(unit == TemperatureInput.Celsius) {
                    text = "°C"
                }
                else if(unit == TemperatureInput.Fahrenheit) {
                    text = "°F"
                }
                else if(unit == TemperatureInput.Kelvin) {
                    text = "K"
                }
            }

            Component.onCompleted: {
                TemperatureInput.unit = TemperatureInput.Celsius
            }
        }

        TemperatureInput.onTemperatureChanged: {
            temperature.text = value
        }
    }

    MouseArea {
        anchors.fill: parent
        onReleased: {
            if(temperatureUnit.unit == TemperatureInput.Celsius) {
                TemperatureInput.setUnit(TemperatureInput.Fahrenheit)
            }
            else if(temperatureUnit.unit == TemperatureInput.Fahrenheit) {
                TemperatureInput.setUnit(TemperatureInput.Kelvin)
            }
            else if(temperatureUnit.unit == TemperatureInput.Kelvin) {
                TemperatureInput.setUnit(TemperatureInput.Celsius)
            }
        }
    }
}

Notice that the application's UI is simple, with a Rectangle as the root item and a couple of Text items for the temperature value and unit. The MouseArea item enables the application to switch temperature units via touch. It also tracks the temperatureChanged signal from the TemperatureInput singleton. The unit property of the temperatureUnit Text item is bound to the unit Qul::Property instance of the TemperatureInput singleton. This lets you change the property from both QML and C++. Finally, touching the screen changes the unit via the setUnit function to enable immediate conversion of current temperature value, without wating for the next read from the sensor.

Rotary knob control input

This example demonstrates how a rotary knob control input can be handled. A rotary knob control is like a control knob you can find from a car, for example, to control the car's user interface. In our example, the rotary knob can be nudged to four directions, down, up, left, and right. It can be pushed, or rotated to left and right, and it has a back button. To keep things simple, the rotation does not have dynamic range and it cannot be rotated full 360°.

Now, introduce a custom RotaryKnobInputEvent type. It is a simple struct inheriting Qul::Object to let you use the type in QML. It introduces the Action enumeration to represent the various actions user can do with the rotary knob. It also introduces ActionState to track the action lifetime with start and stop states, and a repeat state for repeated actions. The constructor of this custom type, takes the action and state parameters. Finally, it has action and state members to store the event data. The header containing RotaryKnobInputEvent must be introduced to QML using the qul_target_generate_interfaces cmake command in the application target.

struct RotaryKnobInputEvent : public Qul::Object
{
    enum Action : int { NudgeDown, NudgeUp, NudgeLeft, NudgeRight, Push, Back, RotateLeft, RotateRight };

    enum ActionState : int { ActionStart, ActionRepeat, ActionStop };

    RotaryKnobInputEvent(Action action, ActionState state)
        : action(action)
        , actionState(state)
    {}

    Action action;
    ActionState actionState;
};

Next, introduce the C++ to QML interface for the knob input. It is a Qul::Singleton with Qul::EventQueue. It introduces one Qul::Signal to send rotary knob action and state to the application. It also overrides the onEvent() function of Qul::EventQueue to receive events from platform and forward them to application via rotaryKnobEvent signal. The interface being a Qul::Singleton, external construction and copying is prevented. The header containing RotaryKnobInput interface must be introduced to QML via qul_target_generate_interfaces Cmake command in the application target.

struct RotaryKnobInput : public Qul::Singleton<RotaryKnobInput>, public Qul::EventQueue<RotaryKnobInputEvent>
{
    Qul::Signal<void(int action, int state)> rotaryKnobEvent;

    void onEvent(const RotaryKnobInputEvent &event) override { rotaryKnobEvent(event.action, event.actionState); }

private:
    // Create friendship for a base class to be able to access private constructor
    friend class Qul::Singleton<RotaryKnobInput>;
    RotaryKnobInput() {}
    RotaryKnobInput(const RotaryKnobInput &);
    RotaryKnobInput &operator=(const RotaryKnobInput &);
};

Now, you can send rotary knob events to the application. Here is a dummy rotaryKnobISR() function that acts as the ISR for the rotary knob input device. It forwards the event using the RotaryKnobInput singleton instance. For testing purposes, it rotates the rotary knob states and actions around. If you don't have a real rotary knob device available, hook it up with some button to play around.

void rotaryKnobISR()
{
    // Here would be platform specific code to read state from rotary knob device.

    static int action = RotaryKnobInputEvent::ActionStart;
    static int state = RotaryKnobInputEvent::NudgeDown;

    RotaryKnobInput::instance().postEvent(
        RotaryKnobInputEvent(RotaryKnobInputEvent::Action(action), RotaryKnobInputEvent::ActionState(state)));

    if (++state > RotaryKnobInputEvent::ActionStop) {
        state = RotaryKnobInputEvent::ActionStart;

        if (++action > RotaryKnobInputEvent::RotateRight)
            action = RotaryKnobInputEvent::NudgeDown;
    }
}

Finally, here is a simple test application that receives the rotary knob events and shows the received events in a Text item.

import QtQuick 2.15

Rectangle {
    color: "#41CD52"

    Text {
        id: knobState
        anchors.centerIn: parent
        font.pixelSize: 30
        text: "Waiting for rotary knob..."
    }

    RotaryKnobInput.onRotaryKnobEvent: {
        knobState.text = knobStateToString(state) + ": " + knobActionToString(action)
    }

    function knobActionToString(action : int) : string {
        if(action == RotaryKnobInputEvent.NudgeDown)
            return "NudgeDown"
        else if(action == RotaryKnobInputEvent.NudgeUp)
            return "NudgeUp"
        else if(action == RotaryKnobInputEvent.NudgeLeft)
            return "NudgeLeft"
        else if(action == RotaryKnobInputEvent.NudgeRight)
            return "NudgeRight"
        else if(action == RotaryKnobInputEvent.Push)
            return "Push"
        else if(action == RotaryKnobInputEvent.Back)
            return "Back"
        else if(action == RotaryKnobInputEvent.RotateLeft)
            return "RotateLeft"
        else if(action == RotaryKnobInputEvent.RotateRight)
            return "RotateRight"
    }

    function knobStateToString(state : int) : string {
        if(state == RotaryKnobInputEvent.ActionStart)
            return "ActionStart"
        else if(state == RotaryKnobInputEvent.ActionRepeat)
            return "ActionRepeat"
        else if(state == RotaryKnobInputEvent.ActionStop)
            return "ActionStop"
    }
}

Available under certain Qt licenses.
Find out more.