cleaned up readme

This commit is contained in:
Atul R 2019-06-16 16:57:44 +02:00
parent 75cfa52836
commit 92a705b663
6 changed files with 75 additions and 109 deletions

View File

@ -1,3 +1,5 @@
# Event handling
In Qt you can respond to an external event like a key press via event handling. Events always are processed by the event loop. Alongside events Qt also has a concept of Signals/Slots. Signals and slots are used to primarily communicate between widgets (more precisely QObjects). So the most common way of interacting between Qt Widgets is done through signals/slots. (More details here: https://doc.qt.io/qt-5/signalsandslots.html). Hence we would be implementing support for both events and signals.
**Technicals:**
@ -17,7 +19,7 @@ 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
# Adding signal/event handling support to a NodeWidget
We will take the example of PushButton
@ -25,23 +27,23 @@ We will take the example of PushButton
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)`.
The widget should inherit from `NodeWidget`. NodeWidget inherits from EventWidget internally. EventWidget 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
EventWidget adds `addEventListener` method to the widget which can be called
like this:
```js
button.setSignalListener("clicked", () => {
button.addEventListener("clicked", () => {
console.log("clicked");
});
```
To help the user know what all signals are supported, export an enum like `QPushButtonSignal` as shown below.
To help the user know what all signals/events are supported, export an enum like `QPushButtonEvents` as shown below.
So the user can then use it as below:
```js
button.setSignalListener(QPushButtonSignal.clicked, () => {
button.addEventListener(QPushButtonEvents.clicked, () => {
console.log("clicked");
});
```
@ -51,16 +53,17 @@ Example:
```js
import addon from "../../core/addon";
import { NodeWidget } from "../../QtGui/QWidget";
import { SignalNodeWidget } from "../../core/SignalNodeWidget";
import { BaseWidgetEvents } from "../../core/EventWidget";
export enum QPushButtonSignal {
clicked = "clicked",
pressed = "pressed",
released = "released",
toggled = "toggled"
}
export const QPushButtonEvents = Object.freeze({
...BaseWidgetEvents,
clicked: "clicked",
pressed: "pressed",
released: "released",
toggled: "toggled"
});
export class QPushButton extends SignalNodeWidget {
export class QPushButton extends NodeWidget {
native: any;
constructor(parent?: NodeWidget) {
let native;
@ -72,10 +75,12 @@ export class QPushButton extends SignalNodeWidget {
super(native);
this.parent = parent;
this.native = native;
// bind member functions
this.setText.bind(this);
}
setText(text: string) {
this.native.setText(text);
setText(text: string | number) {
this.native.setText(`${text}`);
}
}
```
@ -84,110 +89,67 @@ export class QPushButton extends SignalNodeWidget {
Steps:
1. No changes to `NPushButton`
1. `NPushButton`
2. In `qpushbutton_wrap.h`:
Inherit from both QPushButton and NodeWidget. Make sure you have added NODEWIDGET_IMPLEMENTATIONS macro. This adds a crucial method for events support. It will override `event(QEvent *)` method of QPushbutton so that nodejs can listen to the events of this widget. This makes sure we convert all the QEvent's of this widget to an event for the nodejs event emitter.
```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`
Also make sure to connect all the signals of the widgets to the event emitter instance from NodeJS. This way we kindof convert the signal to a simple nodejs event.
```cpp
...
...
#pragma once
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),
...
...
});
...
...
}
#include <QPushButton>
#include "src/cpp/core/NodeWidget/nodewidget.h"
#include "napi.h"
...
...
class NPushButton: public QPushButton, public NodeWidget
{
NODEWIDGET_IMPLEMENTATIONS
public:
using QPushButton::QPushButton; //inherit all constructors of QPushButton
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) };
// override this method and implement all signals here
void connectWidgetSignalsToEventEmitter() {
// Qt Connects: Implement all signal connects here
QObject::connect(this, &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, &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, &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) };
QObject::connect(this, &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`.
Make sure `npushbutton.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.
1. On JS side for each widget instance we create an instance of NodeJS's Event Emitter. This is done by the class `EventWidget` from which `NodeWidget` inherits
2. We send this event emiiter's `emit` function to the C++ side by calling `initNodeEventEmitter` method and store a pointer to the event emitter's emit function using `emitOnNode`. initNodeEventEmitter function is added by a macro from EventWidget (c++). You can find the initNodeEventEmitter method with the event widget macros.
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. This is done manually on every widget by overriding the method `connectWidgetSignalsToEventEmitter`. Check `npushbutton.h` for details. This takes care of all the signals of the widgets. Now to export all qt events of the widget, we had overriden the widgets `event(Event*)` method to listen to events received by the widget and send it to the event emitter. This is done inside the EVENTWIDGET_IMPLEMENTATIONS macro
> 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.
```

View File

@ -15,15 +15,12 @@ class QPushButtonWrap : public Napi::ObjectWrap<QPushButtonWrap> {
QPushButtonWrap(const Napi::CallbackInfo& info);
~QPushButtonWrap();
NPushButton* getInternalInstance();
Napi::Value setupSignalListeners(const Napi::CallbackInfo& info);
//class constructor
static Napi::FunctionReference constructor;
//wrapped methods
Napi::Value setText(const Napi::CallbackInfo& info);
Napi::Value subscribeToEvent(const Napi::CallbackInfo& info);
QWIDGET_WRAPPED_METHODS_DECLARATION
};

View File

@ -1,7 +1,7 @@
#include "eventwidget.h"
#include "deps/spdlog/spdlog.h"
void EventWidget::subscribeToEvent(std::string evtString){
void EventWidget::subscribeToQtEvent(std::string evtString){
try {
int evtType = EventsMap::events.at(evtString);
this->subscribedEvents.insert({static_cast<QEvent::Type>(evtType), evtString});

View File

@ -9,7 +9,7 @@ public:
std::unique_ptr<ThreadSafeCallback> emitOnNode = nullptr;
std::unordered_map<QEvent::Type, std::string> subscribedEvents;
void subscribeToEvent(std::string evtString);
void subscribeToQtEvent(std::string evtString);
void event(QEvent* event);

View File

@ -1,6 +1,7 @@
#pragma once
#include "eventwidget.h"
#include <QWidget>
/*
@ -21,7 +22,7 @@ Napi::Value initNodeEventEmitter(const Napi::CallbackInfo& info) { \
Napi::Value subscribeToQtEvent(const Napi::CallbackInfo& info){ \
Napi::Env env = info.Env(); \
Napi::String eventString = info[0].As<Napi::String>(); \
this->instance->subscribeToEvent(eventString.Utf8Value()); \
this->instance->subscribeToQtEvent(eventString.Utf8Value()); \
return env.Null(); \
} \
@ -36,3 +37,12 @@ Napi::Value subscribeToQtEvent(const Napi::CallbackInfo& info){ \
#endif // EVENTWIDGET_WRAPPED_METHODS_EXPORT_DEFINE
#ifndef EVENTWIDGET_IMPLEMENTATIONS
#define EVENTWIDGET_IMPLEMENTATIONS \
bool event(QEvent* event) { \
EventWidget::event(event); \
return QWidget::event(event); \
} \
#endif //EVENTWIDGET_IMPLEMENTATIONS

View File

@ -1,6 +1,6 @@
#pragma once
#include "src/cpp/core/YogaWidget/yogawidget.h"
#include "src/cpp/core/Events/eventwidget.h"
#include "src/cpp/core/Events/eventwidget_macro.h"
// class to unify all the custom features + add extra features if needed
class NodeWidget : public YogaWidget, public EventWidget {
@ -15,10 +15,7 @@ class NodeWidget : public YogaWidget, public EventWidget {
Q_OBJECT \
public: \
SET_YOGA_WIDGET_Q_PROPERTIES \
bool event(QEvent* event) { \
EventWidget::event(event); \
return QWidget::event(event); \
} \
EVENTWIDGET_IMPLEMENTATIONS \
#endif //NODEWIDGET_IMPLEMENTATIONS