Merge pull request #947 from nodegui/qevent_control

Expand event support to grab QEvents after default processing
This commit is contained in:
Simon Edwards 2022-05-27 18:05:39 +02:00 committed by GitHub
commit 2ece6d5375
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 166 additions and 35 deletions

View File

@ -16,8 +16,13 @@ class DLL_EXPORT EventWidget {
void unSubscribeToQtEvent(std::string evtString);
bool event(QEvent* event);
bool eventAfterDefault(QEvent* event, bool baseWidgetResult);
virtual void connectSignalsToEventEmitter();
~EventWidget();
};
private:
bool sendEventToNode(QEvent* event, bool afterBaseWidget,
bool baseWidgetResult);
};

View File

@ -99,12 +99,13 @@ struct InitHelper {
#endif // EVENTWIDGET_WRAPPED_METHODS_EXPORT_DEFINE
#ifndef EVENTWIDGET_IMPLEMENTATIONS
#define EVENTWIDGET_IMPLEMENTATIONS(BaseWidgetName) \
bool event(QEvent* event) override { \
if (EventWidget::event(event)) { \
return true; \
} \
return BaseWidgetName::event(event); \
#define EVENTWIDGET_IMPLEMENTATIONS(BaseWidgetName) \
bool event(QEvent* event) override { \
if (EventWidget::event(event)) { \
return true; \
} \
bool baseWidgetResult = BaseWidgetName::event(event); \
return EventWidget::eventAfterDefault(event, baseWidgetResult); \
}
#endif // EVENTWIDGET_IMPLEMENTATIONS

View File

@ -29,6 +29,15 @@ void EventWidget::unSubscribeToQtEvent(std::string evtString) {
}
bool EventWidget::event(QEvent* event) {
return sendEventToNode(event, false, false);
}
bool EventWidget::eventAfterDefault(QEvent* event, bool baseWidgetResult) {
return sendEventToNode(event, true, baseWidgetResult);
}
bool EventWidget::sendEventToNode(QEvent* event, bool afterBaseWidget,
bool baseWidgetResult) {
if (this->emitOnNode) {
try {
QEvent::Type evtType = event->type();
@ -37,8 +46,10 @@ bool EventWidget::event(QEvent* event) {
Napi::HandleScope scope(env);
Napi::Value nativeEvent = Napi::External<QEvent>::New(env, event);
std::vector<napi_value> args = {Napi::String::New(env, eventTypeString),
nativeEvent};
std::vector<napi_value> args = {
Napi::String::New(env, eventTypeString), nativeEvent,
Napi::Boolean::New(env, afterBaseWidget),
Napi::Boolean::New(env, baseWidgetResult)};
Napi::Value returnCode = this->emitOnNode.Call(args);
return returnCode.As<Napi::Boolean>().Value();
@ -46,7 +57,7 @@ bool EventWidget::event(QEvent* event) {
// Do nothing
}
}
return false;
return baseWidgetResult;
}
void EventWidget::connectSignalsToEventEmitter() {

View File

@ -7,6 +7,15 @@ function addDefaultErrorHandler(native: NativeElement, emitter: EventEmitter): v
emitter.addListener('error', () => null);
}
export interface EventListenerOptions {
/**
* This applies only when listening to QEvents. If set to true, then the callback will
* be called after the default processing by the base widget has occurred. By default
* callbacks for QEvents are called before the base widget `::event()` is called.
*/
afterDefault?: boolean;
}
/**
> Abstract class that adds event handling support to all widgets.
@ -33,6 +42,7 @@ view.addEventListener(WidgetEventTypes.MouseMove, () => {
});
```
*/
export abstract class EventWidget<Signals extends unknown> extends Component {
private emitter: EventEmitter;
private _isEventProcessed = false;
@ -51,18 +61,43 @@ export abstract class EventWidget<Signals extends unknown> extends Component {
this.emitter = new EventEmitter();
this.emitter.emit = wrapWithActivateUvLoop(this.emitter.emit.bind(this.emitter));
const logExceptions = (event: string | symbol, ...args: any[]): boolean => {
const logExceptions = (eventName: string, ...args: any[]): boolean => {
// Preserve the value of `_isQObjectEventProcessed` as we dispatch this event
// to JS land, and restore it afterwards. This lets us support recursive event
// dispatches on the same object.
const wrappedArgs = args.map(wrapNative);
const previousEventProcessed = this._isEventProcessed;
this._isEventProcessed = false;
try {
this.emitter.emit(event, ...wrappedArgs);
} catch (e) {
console.log(`An exception was thrown while dispatching an event of type '${event.toString()}':`);
console.log(e);
// Events start with a capital letter, signals are lower case by convention.
const firstChar = eventName.charAt(0);
const isQEvent = firstChar.toUpperCase() === firstChar;
if (isQEvent) {
try {
const event = wrapNative(args[0]);
const afterBaseWidget = args[1];
const baseWidgetResult = args[2];
if (!afterBaseWidget) {
this.emitter.emit(eventName, event);
} else {
this._isEventProcessed = baseWidgetResult;
this.emitter.emit(`${eventName}_after`);
}
} catch (e) {
console.log(
`An exception was thrown while dispatching an event of type '${eventName.toString()}':`,
);
console.log(e);
}
} else {
try {
const wrappedArgs = args.map(wrapNative);
this.emitter.emit(eventName, ...wrappedArgs);
} catch (e) {
console.log(
`An exception was thrown while dispatching a signal of type '${eventName.toString()}':`,
);
console.log(e);
}
}
const returnCode = this._isEventProcessed;
@ -107,6 +142,7 @@ export abstract class EventWidget<Signals extends unknown> extends Component {
*
@param signalType SignalType is a signal from the widgets signals interface.
@param callback Corresponding callback for the signal as mentioned in the widget's signal interface
@param options Extra optional options controlling how this event listener is added.
@returns void
For example in the case of QPushButton:
@ -116,23 +152,37 @@ export abstract class EventWidget<Signals extends unknown> extends Component {
// here clicked is a value from QPushButtonSignals interface
```
*/
addEventListener<SignalType extends keyof Signals>(signalType: SignalType, callback: Signals[SignalType]): void;
addEventListener<SignalType extends keyof Signals>(
signalType: SignalType,
callback: Signals[SignalType],
options?: EventListenerOptions,
): void;
/**
@param eventType
@param callback
@param eventType
@param callback
@param options Extra optional options controlling how this event listener is added.
For example in the case of QPushButton:
```js
const button = new QPushButton();
button.addEventListener(WidgetEventTypes.HoverEnter,()=>console.log("hovered"));
```
*/
addEventListener(eventType: WidgetEventTypes, callback: (event?: NativeRawPointer<'QEvent'>) => void): void;
addEventListener(eventOrSignalType: string, callback: (...payloads: any[]) => void): void {
For example in the case of QPushButton:
```js
const button = new QPushButton();
button.addEventListener(WidgetEventTypes.HoverEnter,()=>console.log("hovered"));
```
*/
addEventListener(
eventType: WidgetEventTypes,
callback: (event?: NativeRawPointer<'QEvent'>) => void,
options?: EventListenerOptions,
): void;
addEventListener(
eventOrSignalType: string,
callback: (...payloads: any[]) => void,
options?: EventListenerOptions,
): void {
const eventOrSignalName = options?.afterDefault ? `${eventOrSignalType}_after` : eventOrSignalType;
if (this.native.subscribeToQtEvent(eventOrSignalType)) {
this.emitter.addListener(eventOrSignalType, callback);
this.emitter.addListener(eventOrSignalName, callback);
} else {
try {
throw new Error();
@ -145,15 +195,29 @@ export abstract class EventWidget<Signals extends unknown> extends Component {
}
}
removeEventListener<SignalType extends keyof Signals>(signalType: SignalType, callback: Signals[SignalType]): void;
removeEventListener(eventType: WidgetEventTypes, callback: (event?: NativeRawPointer<'QEvent'>) => void): void;
removeEventListener(eventOrSignalType: string, callback?: (...payloads: any[]) => void): void {
removeEventListener<SignalType extends keyof Signals>(
signalType: SignalType,
callback: Signals[SignalType],
options?: EventListenerOptions,
): void;
removeEventListener(
eventType: WidgetEventTypes,
callback: (event?: NativeRawPointer<'QEvent'>) => void,
options?: EventListenerOptions,
): void;
removeEventListener(
eventOrSignalType: string,
callback?: (...payloads: any[]) => void,
options?: EventListenerOptions,
): void {
const eventOrSignalTypeAfter = `${eventOrSignalType}_after`;
const registeredEventName = options?.afterDefault ? eventOrSignalTypeAfter : eventOrSignalType;
if (callback) {
this.emitter.removeListener(eventOrSignalType, callback);
this.emitter.removeListener(registeredEventName, callback);
} else {
this.emitter.removeAllListeners(eventOrSignalType);
this.emitter.removeAllListeners(registeredEventName);
}
if (this.emitter.listenerCount(eventOrSignalType) < 1) {
if (this.emitter.listenerCount(eventOrSignalType) + this.emitter.listenerCount(eventOrSignalTypeAfter) === 0) {
this.native.unSubscribeToQtEvent(eventOrSignalType);
}
}

View File

@ -0,0 +1,49 @@
---
sidebar_label: Advanced QEvent Handling
title: Advanced QEvent Handling
---
As briefly discussed in [Handle Events](https://docs.nodegui.org/docs/guides/handle-events), Qt and NodeGui have two kinds of event-like things: Signals and QEvents. Most of the time you will just need to listen to signals, but in more advanced situations, such as customizing the behavior of widgets, you may need more control over QEvent processing.
QEvents are often used by Qt to control cross-cutting aspects of the user interface like input, layout, and rendering.
## Preventing Further QEvent Processing
Most widgets in C++ will receive QEvent instances, act on them, and return a boolean indicating if the event is now completely processed or not. If an event is not marked as processed, then Qt may try sending it to another widget such as the parent widget.
NodeGui doesn't allow an event listener function to return a boolean, instead each widget, QObject actually, has a `setEventProcessed()` method which can be used to mark an event as processed.
The example below intercepts the "KeyPress" event on a QLineEdit and cancels the default handling of enter and escapes keys. If first wrap the native event object in the correct JS wrapper class, and then checkes its contents. It accepts the event and cancels further processing via `setEventProcessed(true)`. The QLineEdit itself then never heards about these key presses.
```javascript
const myLineEdit = new QLineEdit();
myLineEdit.addEventListener('KeyPress', (nativeEvent) => {
const event = new QKeyEvent(nativeEvent);
const key = event.key();
if ([Key.Key_Escape, Key.Key_Enter, Key.Key_Return].includes(key)) {
event.accept();
myLineEdit.setEventProcessed(true);
}
});
```
## Listening to QEvents After Default Processing
If an event is not marked as processed by an event listener, then it will be given to the Qt widget for processing. By default, a listener added via `addEventListener()` for a QEvent type, will fire as soon as the event comes in and before the widget has a chance to see it. Sometimes it is desirable to process events *after* the widget has done its processing.
The optional third argument to `addEventListener()`, the options object, has a boolean `afterDefault`. If this is set, then the listener will be called after the widget has processed the event.
This example shows how to perform some extra work immediately after the widget has updated its own layout.
```javascript
const myWidget = new QWidget();
myWidget.addEventListener(WidgetEventTypes.LayoutRequest, () => {
this.doMyLayout();
}, {afterDefault: true});
```
Note: If you later want to remove an event handler with `removeEventListener()`, you will have to pass the same options as used when calling `addEventListener()` initially.

View File

@ -361,6 +361,7 @@ module.exports = {
"guides/debugging",
"guides/debugging-in-vscode",
"guides/understanding-memory",
"guides/advanced-qevent-handling",
"guides/using-native-node-modules",
"guides/custom-nodegui-native-plugin",
"guides/packaging"