Added dev docs about signal handling

This commit is contained in:
Atul R 2019-06-08 22:15:15 +02:00
parent 44a88390fe
commit 0f181775b4

View File

@ -0,0 +1,186 @@
# Implementing Signal handling
In Qt signals and slots are used to communicate between different qt widgets. So they can be used to implement things like
onClick, onHover etc.
The way Qt Signals work is explained here:
https://doc.qt.io/qt-5/signalsandslots.html
The way you use them in Qt for a PushButton is explained here:
https://wiki.qt.io/How_to_Use_QPushButton#Signals
# Adding signal handling support to a NodeWidget
We will take the example of PushButton
**Javascript**
Steps:
The widget should inherit from `SignalNodeWidget` instead of `NodeWidget`. SignalNodeWidget inherits from NodeWidget internally. SignalNodeWidget constructor needs native object while initialising. So arrange your code such that native object gets initialised before calling `super(native)`.
SignalNodeWidget adds `setSignalListener` method to the widget which can be called
like this:
```js
button.setSignalListener("clicked", () => {
console.log("clicked");
});
```
To help the user know what all signals are supported, export an enum like `QPushButtonSignal` as shown below.
So the user can then use it as below:
```js
button.setSignalListener(QPushButtonSignal.clicked, () => {
console.log("clicked");
});
```
Example:
```js
import addon from "../../core/addon";
import { NodeWidget } from "../../QtGui/QWidget";
import { SignalNodeWidget } from "../../core/SignalNodeWidget";
export enum QPushButtonSignal {
clicked = "clicked",
pressed = "pressed",
released = "released",
toggled = "toggled"
}
export class QPushButton extends SignalNodeWidget {
native: any;
constructor(parent?: NodeWidget) {
let native;
if (parent) {
native = new addon.QPushButton(parent.native);
} else {
native = new addon.QPushButton();
}
super(native);
this.parent = parent;
this.native = native;
}
setText(text: string) {
this.native.setText(text);
}
}
```
**C++**
Steps:
1. No changes to `NPushButton`
2. In `qpushbutton_wrap.h`:
```cpp
...
...
#include <napi-thread-safe-callback.hpp>
...
...
class QPushButtonWrap : public Napi::ObjectWrap<QPushButtonWrap> {
private:
...
// This will store our event emitter from JS
std::unique_ptr<ThreadSafeCallback> emitOnNode;
public:
...
...
// This will be called internally by SignalNodeWidget class in JS
Napi::Value setupSignalListeners(const Napi::CallbackInfo& info);
...
...
};
```
3. In `qpushbutton_wrap.cpp`
```cpp
...
...
Napi::Object QPushButtonWrap::init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
char CLASSNAME[] = "QPushButton";
Napi::Function func = DefineClass(env, CLASSNAME, {
...
...
InstanceMethod("setupSignalListeners",&QPushButtonWrap::setupSignalListeners),
...
...
});
...
...
}
...
...
QPushButtonWrap::~QPushButtonWrap() {
this->emitOnNode.release(); //cleanup emitOnNode
delete this->instance;
}
...
...
Napi::Value QPushButtonWrap::setupSignalListeners(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
this->emitOnNode = std::make_unique<ThreadSafeCallback>(info[0].As<Napi::Function>());
// Qt Connects: Implement all signal connects here
QObject::connect(this->instance, &QPushButton::clicked, [=](bool checked) {
this->emitOnNode->call([=](Napi::Env env, std::vector<napi_value>& args) {
args = { Napi::String::New(env, "clicked"), Napi::Value::From(env, checked) };
});
});
QObject::connect(this->instance, &QPushButton::released, [=]() {
this->emitOnNode->call([=](Napi::Env env, std::vector<napi_value>& args) {
args = { Napi::String::New(env, "released") };
});
});
QObject::connect(this->instance, &QPushButton::pressed, [=]() {
this->emitOnNode->call([=](Napi::Env env, std::vector<napi_value>& args) {
args = { Napi::String::New(env, "pressed") };
});
});
QObject::connect(this->instance, &QPushButton::toggled, [=](bool checked) {
this->emitOnNode->call([=](Napi::Env env, std::vector<napi_value>& args) {
args = { Napi::String::New(env, "toggled"), Napi::Value::From(env, checked) };
});
});
return env.Null();
}
...
...
```
**Additional**
Make sure `qpushbutton_wrap.h` is added to `config/moc.json`.
And run `npm run automoc` before running `npm run build:addon`
We need to run Qt's MOC (Meta Object Compiler) on the file whenever we use Q_OBJECT in a class or use QObject::connect. This is so that Qt can expand the macros and add necessary implementations to our class.
# How does it work ?
1. On JS side for each widget instance we create an instance of NodeJS's Event Emitter. This is done by the class `SignalNodeWidget`
2. We send this event emiiter's `emit` function to the C++ side by calling `setupSignalListeners` method and store a pointer to it using `emitOnNode`.
3. We setup Qt's connect method for all the signals that we want to listen to and call the emitOnNode (which is actually emit from Event emitter) whenever a signal arrives.
> Note that we **can't** just store Napi::Function emit directly and use it. This is because we would need access to `Napi::Env` while making a call and there is no way to do it asynchronously.
> Since NAPI (node-addon-api) doesnt support asynchronous callbacks properly yet. (Although work in underway) we use this third party library (https://github.com/mika-fischer/napi-thread-safe-callback) to do so. This library provides us a way to access the Napi::Env variable whenever we need it.