From 43deec1c9e8dd23d08b6c049d5034e1f2ff62eed Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 26 May 2022 09:44:34 +0200 Subject: [PATCH 1/3] Expand event support to grab QEvents after default processing --- .../include/nodegui/core/Events/eventwidget.h | 7 +- .../nodegui/core/Events/eventwidget_macro.h | 13 +- src/cpp/lib/core/Events/eventwidget.cpp | 17 ++- src/lib/core/EventWidget.ts | 114 ++++++++++++++---- 4 files changed, 116 insertions(+), 35 deletions(-) diff --git a/src/cpp/include/nodegui/core/Events/eventwidget.h b/src/cpp/include/nodegui/core/Events/eventwidget.h index 0fc90c87f..b9b27f93d 100644 --- a/src/cpp/include/nodegui/core/Events/eventwidget.h +++ b/src/cpp/include/nodegui/core/Events/eventwidget.h @@ -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(); -}; \ No newline at end of file + + private: + bool sendEventToNode(QEvent* event, bool afterBaseWidget, + bool baseWidgetResult); +}; diff --git a/src/cpp/include/nodegui/core/Events/eventwidget_macro.h b/src/cpp/include/nodegui/core/Events/eventwidget_macro.h index 3afaeb39b..b1e8f654b 100644 --- a/src/cpp/include/nodegui/core/Events/eventwidget_macro.h +++ b/src/cpp/include/nodegui/core/Events/eventwidget_macro.h @@ -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 \ No newline at end of file diff --git a/src/cpp/lib/core/Events/eventwidget.cpp b/src/cpp/lib/core/Events/eventwidget.cpp index ca753af25..25a5e0711 100644 --- a/src/cpp/lib/core/Events/eventwidget.cpp +++ b/src/cpp/lib/core/Events/eventwidget.cpp @@ -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::New(env, event); - std::vector args = {Napi::String::New(env, eventTypeString), - nativeEvent}; + std::vector 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().Value(); @@ -46,7 +57,7 @@ bool EventWidget::event(QEvent* event) { // Do nothing } } - return false; + return baseWidgetResult; } void EventWidget::connectSignalsToEventEmitter() { diff --git a/src/lib/core/EventWidget.ts b/src/lib/core/EventWidget.ts index 583a5e5aa..34a7f3d95 100644 --- a/src/lib/core/EventWidget.ts +++ b/src/lib/core/EventWidget.ts @@ -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 extends Component { private emitter: EventEmitter; private _isEventProcessed = false; @@ -51,18 +61,43 @@ export abstract class EventWidget 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 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 extends Component { // here clicked is a value from QPushButtonSignals interface ``` */ - addEventListener(signalType: SignalType, callback: Signals[SignalType]): void; + addEventListener( + 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 extends Component { } } - removeEventListener(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: 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); } } From 755156c19adef4483436803f251408ab6d99140c Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 26 May 2022 11:44:02 +0200 Subject: [PATCH 2/3] Add a Guide about advanced QEvent handling --- .../docs/guides/advanced-qevent-handling.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 website/docs/guides/advanced-qevent-handling.md diff --git a/website/docs/guides/advanced-qevent-handling.md b/website/docs/guides/advanced-qevent-handling.md new file mode 100644 index 000000000..3988f1e61 --- /dev/null +++ b/website/docs/guides/advanced-qevent-handling.md @@ -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. From 421929c14799ae43773e89a08c11abab10674482 Mon Sep 17 00:00:00 2001 From: Simon Edwards Date: Thu, 26 May 2022 12:02:07 +0200 Subject: [PATCH 3/3] Add "Advanced QEvent Handling" guide to the website sidebar --- website/sidebars.js | 1 + 1 file changed, 1 insertion(+) diff --git a/website/sidebars.js b/website/sidebars.js index 904662fcb..dfec983d7 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -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"