diff --git a/devdocs/signal_handling.md b/devdocs/signal_handling.md index e69de29bb..d7e2003d3 100644 --- a/devdocs/signal_handling.md +++ b/devdocs/signal_handling.md @@ -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 + ... + ... + + class QPushButtonWrap : public Napi::ObjectWrap { + private: + ... + // This will store our event emitter from JS + std::unique_ptr 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(info[0].As()); + // Qt Connects: Implement all signal connects here + QObject::connect(this->instance, &QPushButton::clicked, [=](bool checked) { + this->emitOnNode->call([=](Napi::Env env, std::vector& 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& args) { + args = { Napi::String::New(env, "released") }; + }); + }); + QObject::connect(this->instance, &QPushButton::pressed, [=]() { + this->emitOnNode->call([=](Napi::Env env, std::vector& args) { + args = { Napi::String::New(env, "pressed") }; + }); + }); + QObject::connect(this->instance, &QPushButton::toggled, [=](bool checked) { + this->emitOnNode->call([=](Napi::Env env, std::vector& 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.