187 lines
5.5 KiB
Markdown
187 lines
5.5 KiB
Markdown
# 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.
|