C

Handling custom input devices

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 interface defined is a Qul::Singleton, external construction and copying is prevented.

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) QUL_DECL_OVERRIDE
    {
        temperatureChanged(temperature);
        cachedValue = temperature;
    }

private:
    // Create friendship for a base class to be able to access private constructor
    friend struct 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().postEventFromInterrupt(30.0);
        break;
    case TemperatureInput::Fahrenheit:
        TemperatureInput::instance().postEventFromInterrupt(86.0);
        break;
    case TemperatureInput::Kelvin:
        TemperatureInput::instance().postEventFromInterrupt(303.15);
    }
}

The header containing the C++ to QML interface class must be introduced to QML using the InterfaceFiles.files in the QmlProject file:

Project {
    ...
    InterfaceFiles {
        files: ["temperatureinput.h"]
    }
}

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

import QtQuick 2.15

Rectangle {
    Text {
        id: temperature
        anchors.centerIn: parent
        text: "0"

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

            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.

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

    enum ActionState { ActionStart, ActionRepeat, ActionStop };

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

    RotaryKnobInputEvent()
        : action(NudgeDown)
        , actionState(ActionStart)
    {}

    Action action;
    ActionState actionState;
};

The header containing RotaryKnobInputEvent must be introduced to QML using the InterfaceFiles.files in the QmlProject file:

Project {
    ...
    InterfaceFiles {
        files: ["rotaryknobinputevent.h"]
    }
}

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.

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

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

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

The header containing RotaryKnobInput interface must be introduced to QML via InterfaceFiles.files:

Project {
    ...
    InterfaceFiles {
        files: ["rotaryknobinputevent.h",
                "rotaryknobinput.h"
               ]
    }
}

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().postEventFromInterrupt(
        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 {
    Text {
        id: knobState
        anchors.centerIn: parent
        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.