Saturday, April 27, 2013

QPlatformInputContext and virtual keyboard mockup


Imagine that you're developer preferring Qt, and:
  • you convinced your manager to try out Qt/Qt Quick as UI framework for your custom device (cash machine, POS, ticket machine etc.) to port some old app. 
  • it lacks hardware and software keyboard (the original application's hardcoded its keyboard inside so you can't reuse it).
  • main Qt5 modules are working on the target device (QtCore, QtGui, QtQuick) 
  • you have to provide demo of Qt Quick features on such device (animations, transitions, particles).  
  • deadline is yesterday.
  • you know that if you provide some forms with text input fields and virtual keyboard along with animations and other fireworks then the status of the meeting will change from demo/POC to pre-alpha presentation :)
Animations, particles, transitions are very easy to implement (you could also take sources of Qt Cinematic Experience and change according to needs). The only question is - how fast one will be able to implement mockup of virtual keyboard and reuse it in Qt Quick text editors? Of course Qt handles some standard input systems like Maliit or IBus but they have some dependencies (ex. D-Bus, python). Assuming that our platform doesn't have Maliit or IBus services installed, the only way to achieve the goal is to write some virtual keyboard from scratch. Q Platform Abstraction is the best way to resolve our problem. It's not  officially documented yet but it's self-explanatory API simplifies adding some platform specific features to Qt. Following steps will show how to create custom virtual keyboard that will work with all standard Qt editors (Qt Quick and QtWidgets).

The main classes are QPlatformInputContext and QPlatformInputContextPlugin:

class Q_GUI_EXPORT QPlatformInputContext : public QObject
{
    Q_OBJECT
    Q_DECLARE_PRIVATE(QPlatformInputContext)

public:
    QPlatformInputContext();
    virtual ~QPlatformInputContext();

    virtual bool isValid() const;

    virtual void reset();
    virtual void commit();
    virtual void update(Qt::InputMethodQueries);
    virtual void invokeAction(QInputMethod::Action, int cursorPosition);
    virtual bool filterEvent(const QEvent *event);
    virtual QRectF keyboardRect() const;
    void emitKeyboardRectChanged();

    virtual bool isAnimating() const;
    void emitAnimatingChanged();

    virtual void showInputPanel();
    virtual void hideInputPanel();
    virtual bool isInputPanelVisible() const;
    void emitInputPanelVisibleChanged();

    virtual QLocale locale() const;
    void emitLocaleChanged();
    virtual Qt::LayoutDirection inputDirection() const;
    void emitInputDirectionChanged(Qt::LayoutDirection newDirection);

    virtual void setFocusObject(QObject *object);
    bool inputMethodAccepted() const;

private:
    friend class QGuiApplication;
    friend class QGuiApplicationPrivate;
    friend class QInputMethod;
};

class Q_GUI_EXPORT QPlatformInputContextPlugin : public QObject
{
    Q_OBJECT
public:
    explicit QPlatformInputContextPlugin(QObject *parent = 0);
    ~QPlatformInputContextPlugin();

    virtual QPlatformInputContext *create(const QString &key, const QStringList &paramList) = 0;
};

QPlatformInputContextPlugin registers QPlatformInputContext in Qt way. QPlatformInputContext on the other hand is the magic class used for implementing virtual keyboard. In our case we need to overload only  few methods.


class MockupInputContext : public QPlatformInputContext
{
    Q_OBJECT
public:
    MockupInputContext();
    ~MockupInputContext();
    //return true if plugin is enabled
    bool isValid() const;

    //this value will be available in QGuiApplication::inputMethod()->keyboardRectangle()
    QRectF keyboardRect() const;

    //show and hide are invoked by Qt when editor gets focus
    void showInputPanel();
    void hideInputPanel();
    //this value will be available in QGuiApplication::inputMethod()->isVisible()
    bool isInputPanelVisible() const;

    //editor pointer
    void setFocusObject(QObject *object);

private:
    MockupInputContextPrivate *d;
};

I reviewed some relevant bitsof the Qt sources to know the meaning of the virtual methods. I will explain it later. First look at QInputMethod documentation and it's Qt Quick equivalent. This class is much related to QPlatformInputContext.
  • isValid() - true if the input method (eg. The virtual keyboard) is valid. We could come up with some cases where keyboard is invalid (in client server architecture for example - no connection to the server).  
  • keyboardRect() - virtual keyboard geometry. QGuiApplication::inputMethod()->keyboardRect() returns this value. 
  • showInputPanel() - this method is invoked by the internal Qt input system when any editor or widget gains focus.
  • hideInputPanel() - this method is invoked by the internal Qt input system when any editor or widget loses its focus or QGuiApplication::inputMethod()->setVisible(false) is called.
  • isInputPanelVisible() - true if the virtual keyboard is visible.
Implementation of MockupInputContext is very simple (has about 100 lines). In short the MockupInputContext creates and/or shows QQuickView when showInputPanel() is invoked by internal Qt mechanism. QQuickView loads inputPanel.qml. This QML file contains keyboard UI (I copied the layout from android keyboard). Additionally the MockupKeyEventDispatcher object is exposed to QQmlContext as global context property (keyEventDispatcher) and is used in onClick handlers to create input events.

class MockupKeyEventDispatcher : public QObject
{
    Q_OBJECT
public:
    explicit MockupKeyEventDispatcher(QObject *parent = 0);
    
signals:
    
public slots:
    void setFocusItem(QObject *focusItem);
    void sendKeyToFocusItem(const QString &keyText);
private:
    QObject * m_focusItem;
};

The whole source code is available in my github repository under MockupVirtualKeyboard. It's pure Qt5 project. Make install command installs this plugin in $$[QT_INSTALL_PLUGINS]/platforminputcontexts directory. I have tested in on my desktop with some simple Qt Quicks projects and Qt Designer (see the screenshot). Worked as expected. To turn off this virtual keyboard you can remove plugin from the mentioned directory.
MockupVirtualKeyboard

Summary:

Once again I was astounded by Qt well-thought-out API and its simplicity. Actually writing this post took me more time than development of virtual keyboard. Below are some statistics that speak for themselves:
Time spent 12h
C++ code 164 lines
QML code 245 lines