Sunday, August 16, 2015

BlackBerry 10: Tracking the Virtual Keyboard from Cascades

There are things in BlackBerry 10 that are relevant to the UI of an application, which are not directly exposed via the API for the Cascades UI framework. One such thing is the state of the virtual keyboard. While you can infer this by the focus state of the text fields on a screen, that only takes you so far. Sometimes you really just need to know whether the keyboard is visible and how tall it is. Fortunately, they actually did expose an API for this through BPS.

What I'll be showing in this article is how you can wrap this event-providing BPS API for use from within a Cascades UI. In order to do this, we'll have to do several things:



To do this, we'll be doing the following:

  • Implement AbstractBpsEventHandler in a class that:
    • Subscribes to BPS virtual keyboard events
    • Handles those events to monitor virtual keyboard changes
    • Keeps track of the last known virtual keyboard visibility and height
  • Implement a QObject subclass that:
    • Provides Qt properties for current virtual keyboard state
    • Provides Qt signals for changes to virtual keyboard state
    • Is registered for creation from within QML
In a simple case, we could implement both of these within the same class. However, there's a problem with this. BPS event subscriptions are thread-global, which means having two simultaneous instances of this class could become problematic. Specifically, this means one instance could inadvertently unsubscribe the other.

One solution to this problem is to do a variation on the C++ pImpl pattern. Specifically, what we'll do is:
  • Create a "private" class that implements AbstractBpsEventHandler
    • Register this class as a singleton using Q_GLOBAL_STATIC
  • Create a "public" class that subclasses QObject
    • Have this class interact with the single private class for virtual keyboard state and events
With this approach, we can create as many instances of the public class as we want, while only having a single instance of the class that actually handles the BPS events.

Public declaration

class VirtualKeyboard : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged FINAL)
    Q_PROPERTY(int height READ height NOTIFY heightChanged FINAL)
public:
    explicit VirtualKeyboard(QObject *parent=0);
    virtual ~VirtualKeyboard();
    bool visible() const;
    int height() const;
signals:
    void visibleChanged();
    void heightChanged();
private:
    Q_DISABLE_COPY(VirtualKeyboard)
};

Registering with Qt

qmlRegisterType<VirtualKeyboard>("ex.controls", 1, 0, "VirtualKeyboard");

Using from QML

import bb.cascades 1.0
import ex.controls 1.0

Page {
    Container {
        TextArea {
        }
        Label {
            text: "Keyboard visible: " + virtualKeyboard.visible
        }
        Label {
            text: "Keyboard height: " + virtualKeyboard.height
        }
    }
    attachedObjects: [
        VirtualKeyboard {
            id: virtualKeyboard
            onVisibleChanged: {
                // do stuff
            }
            onHeightChanged: {
                // do stuff
            }
        }
    ]
}

Private declaration

class VirtualKeyboardPrivate : public bb::AbstractBpsEventHandler
{
public:
    VirtualKeyboardPrivate();
    virtual ~VirtualKeyboardPrivate();
    void addInstance(VirtualKeyboard *instance);
    void removeInstance(VirtualKeyboard *instance);

    virtual void event(bps_event_t *event);

    bool started_;
    bool visible_;
    int height_;

private:
    void start();
    void stop();
    QSet<VirtualKeyboard *> instances_;
};

Q_GLOBAL_STATIC(VirtualKeyboardPrivate, globalVirtualKeyboardPrivate)

Private implementation

VirtualKeyboardPrivate::VirtualKeyboardPrivate() : started_(false), visible_(false), height_(0)
{
    subscribe(virtualkeyboard_get_domain());
    bps_initialize();
}

VirtualKeyboardPrivate::~VirtualKeyboardPrivate()
{
    stop();
    bps_shutdown();
}

void VirtualKeyboardPrivate::addInstance(VirtualKeyboard *instance)
{
    instances_.insert(instance);
    start();
}

void VirtualKeyboardPrivate::removeInstance(VirtualKeyboard *instance)
{
    instances_.remove(instance);
    if(instances_.isEmpty()) {
        stop();
    }
}

void VirtualKeyboardPrivate::start()
{
    if(!started_) {
        virtualkeyboard_request_events(0);
        virtualkeyboard_get_height(&height_);
        started_ = true;
    }
}

void VirtualKeyboardPrivate::stop()
{
    if(started_) {
        virtualkeyboard_stop_events(0);
        visible_ = false;
        height_ = 0;
        started_ = false;
    }
}

void VirtualKeyboardPrivate::event(bps_event_t *event)
{
    if(bps_event_get_domain(event) == virtualkeyboard_get_domain()) {
        unsigned int eventCode = bps_event_get_code(event);
        if(eventCode == VIRTUALKEYBOARD_EVENT_VISIBLE) {
            if(!visible_) {
                visible_ = true;
                foreach(VirtualKeyboard *instance, instances_) {
                    QMetaObject::invokeMethod(instance, "visibleChanged");
                }
            }
        }
        else if(eventCode == VIRTUALKEYBOARD_EVENT_HIDDEN) {
            if(visible_) {
                visible_ = false;
                foreach(VirtualKeyboard *instance, instances_) {
                    QMetaObject::invokeMethod(instance, "visibleChanged");
                }
            }
        }
        else if(eventCode == VIRTUALKEYBOARD_EVENT_INFO) {
            int height = virtualkeyboard_event_get_height(event);
            if(height_ != height) {
                height_ = height;
                foreach(VirtualKeyboard *instance, instances_) {
                    QMetaObject::invokeMethod(instance, "heightChanged");
                }
            }
        }
    }
}

Public implementation

VirtualKeyboard::VirtualKeyboard(QObject *parent) : QObject(parent)
{
    VirtualKeyboardPrivate *d = globalVirtualKeyboardPrivate();
    d->addInstance(this);
}

VirtualKeyboard::~VirtualKeyboard()
{
    VirtualKeyboardPrivate *d = globalVirtualKeyboardPrivate();
    d->removeInstance(this);
}

bool VirtualKeyboard::visible() const
{
    VirtualKeyboardPrivate *d = globalVirtualKeyboardPrivate();
    return d->visible_;
}

int VirtualKeyboard::height() const
{
    VirtualKeyboardPrivate *d = globalVirtualKeyboardPrivate();
    return d->height_;
}

No comments: