@gjsify/node-gi 0.13.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +818 -5
- package/binding.gyp +55 -19
- package/cairo.d.ts +187 -0
- package/cairo.js +570 -0
- package/gettext.d.ts +65 -0
- package/gettext.js +128 -0
- package/gi.d.ts +178 -0
- package/gi.js +2018 -23
- package/globals.d.ts +7 -33
- package/globals.js +40 -51
- package/gtk-runtime.d.ts +17 -0
- package/gtk-runtime.js +245 -0
- package/index.d.ts +306 -12
- package/index.js +447 -14
- package/overrides/_signals.js +167 -0
- package/overrides/gio-dbus.js +749 -0
- package/overrides/mainloop.js +60 -0
- package/package.json +45 -4
- package/src/addon.cc +98 -2074
- package/src/cairo.cc +926 -0
- package/src/calls.cc +1425 -0
- package/src/class.cc +1170 -0
- package/src/common.h +624 -0
- package/src/loop.cc +766 -0
- package/src/marshal.cc +1738 -0
- package/src/object.cc +814 -0
- package/src/private.cc +351 -0
- package/src/repo.cc +297 -0
- package/src/signals.cc +535 -0
- package/src/template.cc +116 -0
- package/src/toggle.cc +718 -0
- package/src/variant.cc +587 -0
- package/system.d.ts +49 -0
- package/system.js +116 -0
package/src/signals.cc
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Signals: JS closures, connect/emit/disconnect + the Gtk.Widget composite-template callback scope.
|
|
3
|
+
|
|
4
|
+
#include "common.h"
|
|
5
|
+
|
|
6
|
+
namespace nodegi {
|
|
7
|
+
|
|
8
|
+
// ---- signals (milestone 1) ----
|
|
9
|
+
//
|
|
10
|
+
// A GClosure that wraps a JS callback. The callback is held by a strong
|
|
11
|
+
// napi_ref; the closure's finalize notifier drops it. The generic marshal
|
|
12
|
+
// converts the signal's GValue params to JS — the EMITTER instance (param 0)
|
|
13
|
+
// first, then the signal's own params (1..n), matching GJS — and the JS return
|
|
14
|
+
// into the signal return GValue.
|
|
15
|
+
//
|
|
16
|
+
// Narrowed-leak semantics (with the toggle-ref bridge): a handler that does NOT
|
|
17
|
+
// close over its own object is now collectable once C is the sole owner
|
|
18
|
+
// (toggle-down → weak → GC). A handler that DOES close over its object forms a
|
|
19
|
+
// GObject -> GClosure -> napi_ref -> callback -> wrapper -> handle cycle the GC
|
|
20
|
+
// cannot break across C, so it keeps the object alive until disconnect — which is
|
|
21
|
+
// the GJS-faithful contract (a connected self-referential handler is a reason to
|
|
22
|
+
// stay alive). disconnect drops the closure's napi_ref (JsClosureFinalize),
|
|
23
|
+
// breaking the cycle so the next GC collects the object. The callback ref stays
|
|
24
|
+
// STRONG while connected (a connected handler must fire). Verified by the
|
|
25
|
+
// signal-cycle case in test/gc-identity.test.mjs.
|
|
26
|
+
|
|
27
|
+
struct JsClosureData {
|
|
28
|
+
napi_env env;
|
|
29
|
+
napi_ref callback;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
static void JsClosureFinalize(gpointer data, GClosure* /*closure*/) {
|
|
33
|
+
JsClosureData* jc = static_cast<JsClosureData*>(data);
|
|
34
|
+
if (jc == nullptr) return;
|
|
35
|
+
if (jc->callback != nullptr) napi_delete_reference(jc->env, jc->callback);
|
|
36
|
+
g_free(jc);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static void JsClosureMarshalImpl(GClosure* closure, GValue* return_value, guint n_param_values,
|
|
40
|
+
const GValue* param_values, bool unboxNestedValues) {
|
|
41
|
+
JsClosureData* jc = static_cast<JsClosureData*>(closure->data);
|
|
42
|
+
if (jc == nullptr || jc->callback == nullptr) return;
|
|
43
|
+
// ENV-TEARDOWN GATE: a signal emitted by a dispose cascade at env teardown (or
|
|
44
|
+
// on a terminating worker) reaches this marshal when the env may no longer
|
|
45
|
+
// enter JS; the GValue marshalling / napi_call_function would then abort via
|
|
46
|
+
// node-addon-api's noexcept throw path. Skip the handler — GJS likewise never
|
|
47
|
+
// dispatches JS during GC/context teardown (see toggle.cc NodeGiJsAvailable).
|
|
48
|
+
if (!NodeGiJsAvailable(jc->env)) {
|
|
49
|
+
if (NodeGiToggleDebugEnabled())
|
|
50
|
+
NodeGiToggleDebugLog("signal closure skipped: JS unavailable on env %p (teardown/terminate)",
|
|
51
|
+
static_cast<void*>(jc->env));
|
|
52
|
+
// The probe is also false when a JS exception is PENDING on a live env. Keep
|
|
53
|
+
// the pre-existing depth semantics: a loop-dispatched closure (depth 0)
|
|
54
|
+
// surfaces + clears so the pump stays alive; a synchronous emit leaves it
|
|
55
|
+
// pending for the JS emit() caller. At real teardown Surface... is a no-op
|
|
56
|
+
// (nothing pending / napi reads fail gracefully).
|
|
57
|
+
if (g_syncEmitDepth == 0) SurfacePendingException(jc->env, "signal handler");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
Napi::Env env(jc->env);
|
|
61
|
+
Napi::HandleScope scope(env);
|
|
62
|
+
// JS dispatched from a pump-driven context iteration must not inherit the
|
|
63
|
+
// pump's in-iteration flag (see NodeGiPumpJsDispatchScope in common.h).
|
|
64
|
+
NodeGiPumpJsDispatchScope pumpWindow;
|
|
65
|
+
|
|
66
|
+
napi_value cbv = nullptr;
|
|
67
|
+
if (napi_get_reference_value(jc->env, jc->callback, &cbv) != napi_ok || cbv == nullptr) return;
|
|
68
|
+
|
|
69
|
+
// GJS parity: the JS handler receives the EMITTER (the object the signal was
|
|
70
|
+
// emitted on, param_values[0]) as its first argument, then the signal's own
|
|
71
|
+
// parameters (param_values[1..n]). GJS passes the emitter first (refs/gjs
|
|
72
|
+
// gi/value.cpp / object signal invocation), so a positional handler such as
|
|
73
|
+
// `(listbox, row) => …` / `_onRowSelected(_listbox, row)` binds correctly. For
|
|
74
|
+
// a `notify::x` emission the args are therefore (object, pspec), as in GJS.
|
|
75
|
+
std::vector<napi_value> args;
|
|
76
|
+
args.reserve(n_param_values);
|
|
77
|
+
for (guint i = 0; i < n_param_values; i++) {
|
|
78
|
+
// Generic (IN-arg) closures mirror gjs's closure marshal (refs/gjs/gi/
|
|
79
|
+
// value.cpp gjs_value_from_g_value): a G_TYPE_VALUE-boxed param is UNBOXED to
|
|
80
|
+
// the contained value (recursively), so e.g. GIMarshallingTests-style
|
|
81
|
+
// `g_closure_invoke` consumers hand the JS function plain values, exactly as
|
|
82
|
+
// GJS does. Signal closures keep the existing param conversion untouched.
|
|
83
|
+
const GValue* pv = ¶m_values[i];
|
|
84
|
+
if (unboxNestedValues) {
|
|
85
|
+
while (G_VALUE_HOLDS(pv, G_TYPE_VALUE)) {
|
|
86
|
+
const GValue* inner = static_cast<const GValue*>(g_value_get_boxed(pv));
|
|
87
|
+
if (inner == nullptr) break;
|
|
88
|
+
pv = inner;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
Napi::Value v = GValueToJs(env, pv);
|
|
92
|
+
if (env.IsExceptionPending()) {
|
|
93
|
+
// An arg-marshal failure: propagate to a synchronous emit() caller, or
|
|
94
|
+
// surface + clear when dispatched from the loop (see g_syncEmitDepth).
|
|
95
|
+
if (g_syncEmitDepth == 0) SurfacePendingException(jc->env, "signal handler");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
args.push_back(v);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
napi_value result = nullptr;
|
|
102
|
+
napi_status st;
|
|
103
|
+
if (g_syncEmitDepth > 0) {
|
|
104
|
+
// Synchronous JS emit() — JS frames sit below, so the microtask checkpoint
|
|
105
|
+
// is skipped at this depth ANYWAY (make_callback nesting semantics), and
|
|
106
|
+
// plain napi_call_function is therefore checkpoint-equivalent here. It is
|
|
107
|
+
// also load-bearing for cross-runtime parity: Bun's napi_make_callback
|
|
108
|
+
// SWALLOWS a synchronous handler throw (returns napi_ok with nothing
|
|
109
|
+
// pending), which silently broke the "a handler exception propagates out
|
|
110
|
+
// of emit()" contract on Bun (conformance-subset signal tests, PR #766 CI)
|
|
111
|
+
// while Node kept propagating. call_function leaves the pending exception
|
|
112
|
+
// on the env on all three runtimes, so the JS emit() caller rethrows it.
|
|
113
|
+
st = napi_call_function(jc->env, env.Undefined(), cbv, args.size(), args.data(), &result);
|
|
114
|
+
} else {
|
|
115
|
+
// Loop-dispatched (no JS emit() caller below): napi_make_callback runs the
|
|
116
|
+
// nextTick + microtask checkpoint at the callback boundary — the same
|
|
117
|
+
// contract the GI-callback (calls.cc) and vfunc (class.cc) trampolines
|
|
118
|
+
// already follow, and GJS semantics exactly (SpiderMonkey drains the
|
|
119
|
+
// promise-job queue when the last JS frame exits). Without it, promise
|
|
120
|
+
// continuations resolved by a loop-dispatched signal handler linger until
|
|
121
|
+
// the libuv↔GLib bridge's prepare-phase drain — and running GTK-touching
|
|
122
|
+
// JS from a GSource PREPARE phase mid-iteration breaks the GDK frame clock
|
|
123
|
+
// (the Excalibur-on-node-gi stall: `engine.start()`'s load chain, queued
|
|
124
|
+
// inside the GLArea 'render' dispatch, ran from prepare and froze all
|
|
125
|
+
// further ticks/renders). Receiver: `global`, matching the other
|
|
126
|
+
// trampolines (make_callback requires an object receiver; handler `this`
|
|
127
|
+
// is not part of the signal contract).
|
|
128
|
+
napi_value global = nullptr;
|
|
129
|
+
napi_get_global(jc->env, &global);
|
|
130
|
+
g_loopDispatchDepth++;
|
|
131
|
+
st = napi_make_callback(jc->env, nullptr, global, cbv, args.size(), args.data(), &result);
|
|
132
|
+
g_loopDispatchDepth--;
|
|
133
|
+
}
|
|
134
|
+
if (st != napi_ok) {
|
|
135
|
+
// The handler threw. A synchronous emitSignal() (g_syncEmitDepth > 0) must
|
|
136
|
+
// propagate it to the JS emit() caller (node-gi contract). At depth 0 there
|
|
137
|
+
// is no JS caller to catch it, so it is surfaced + cleared (GJS-style) to keep
|
|
138
|
+
// the GLib/libuv loop alive instead of wedging on the never-cleared pending
|
|
139
|
+
// exception.
|
|
140
|
+
//
|
|
141
|
+
// BY DESIGN (GJS-aligned): depth 0 is NOT only the GLib loop's own dispatch (a
|
|
142
|
+
// GTK click, a GtkBuilder template handler) — it ALSO covers synchronous-but-
|
|
143
|
+
// not-emitSignal triggers: a notify:: from g_object_set / a property set, or an
|
|
144
|
+
// inline C emit such as action.activate(). Those reach this marshal with no
|
|
145
|
+
// emitSignal() frame on the stack (depth 0), so a handler throw is logged +
|
|
146
|
+
// swallowed here rather than rethrown to the JS code that triggered the emit —
|
|
147
|
+
// exactly as GJS reports an uncaught signal-handler exception (gjs_log_exception)
|
|
148
|
+
// instead of surfacing it to the trigger site.
|
|
149
|
+
if (g_syncEmitDepth == 0) SurfacePendingException(jc->env, "signal handler");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (return_value != nullptr && (G_VALUE_TYPE(return_value) != G_TYPE_INVALID)) {
|
|
154
|
+
JsToGValue(env, Napi::Value(jc->env, result), return_value);
|
|
155
|
+
// The return marshal must never leave a pending JS exception across the
|
|
156
|
+
// C boundary of a loop-dispatched invocation (same contract as the arg/call
|
|
157
|
+
// paths above): surface + clear it so the GLib/libuv pump stays alive.
|
|
158
|
+
if (env.IsExceptionPending() && g_syncEmitDepth == 0) {
|
|
159
|
+
SurfacePendingException(jc->env, "signal handler");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Cross-runtime microtask checkpoint (Bun/Deno — no-op on Node, see loop.cc):
|
|
164
|
+
// an outermost loop-dispatched closure (a DBus method-call/property vtable
|
|
165
|
+
// closure, a GTK signal, a notify::) is where promise continuations it queued
|
|
166
|
+
// must drain — the async-DBus-reply `retval.then(...)` chain in gio-dbus.js
|
|
167
|
+
// never ran on Bun/Deno while a blocking run() owned the thread (their
|
|
168
|
+
// napi_make_callback performs no checkpoint), timing out the client. Only the
|
|
169
|
+
// depth-0 path drains: a synchronous emit() (depth > 0) keeps its
|
|
170
|
+
// call_function + pending-exception contract untouched, and JS frames below
|
|
171
|
+
// it drain when that stack unwinds.
|
|
172
|
+
if (g_syncEmitDepth == 0 && g_loopDispatchDepth == 0) NodeGiMaybeDrainMicrotasks(jc->env);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Signal-closure marshal (`.connect()`, template callbacks): params as-is.
|
|
176
|
+
static void JsClosureMarshal(GClosure* closure, GValue* return_value, guint n_param_values,
|
|
177
|
+
const GValue* param_values, gpointer /*invocation_hint*/,
|
|
178
|
+
gpointer /*marshal_data*/) {
|
|
179
|
+
JsClosureMarshalImpl(closure, return_value, n_param_values, param_values, false);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Generic-closure marshal (a JS fn marshalled as a GClosure IN-arg, invoked by
|
|
183
|
+
// arbitrary C via g_closure_invoke): identical to the signal marshal except that
|
|
184
|
+
// G_TYPE_VALUE-boxed params are unboxed to their contained value, mirroring gjs's
|
|
185
|
+
// gjs_value_from_g_value (see JsClosureMarshalImpl).
|
|
186
|
+
static void JsGenericClosureMarshal(GClosure* closure, GValue* return_value, guint n_param_values,
|
|
187
|
+
const GValue* param_values, gpointer /*invocation_hint*/,
|
|
188
|
+
gpointer /*marshal_data*/) {
|
|
189
|
+
JsClosureMarshalImpl(closure, return_value, n_param_values, param_values, true);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// JS function → a floating marshaled GClosure (the engine's twin of GJS's
|
|
193
|
+
// Gjs::Closure::create_marshaled for "boxed" closure args — refs/gjs/gi/
|
|
194
|
+
// arg-cache.cpp GClosureInTransferNone::in). The closure holds a strong ref to
|
|
195
|
+
// the function; JsClosureFinalize drops it when the closure is invalidated /
|
|
196
|
+
// finalized (the callee released its last ref, or the invoke-site released the
|
|
197
|
+
// only ref because the callee never kept one). Ref/sink + release policy lives at
|
|
198
|
+
// the marshalling site (marshal.cc / calls.cc CreatedClosures).
|
|
199
|
+
GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn) {
|
|
200
|
+
JsClosureData* jc = g_new0(JsClosureData, 1);
|
|
201
|
+
jc->env = env;
|
|
202
|
+
napi_create_reference(env, fn, 1, &jc->callback);
|
|
203
|
+
GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
|
|
204
|
+
g_closure_set_marshal(closure, JsGenericClosureMarshal);
|
|
205
|
+
g_closure_add_finalize_notifier(closure, jc, JsClosureFinalize);
|
|
206
|
+
return closure;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// connectSignal(handle, signalName, callback, after?) -> handlerId
|
|
210
|
+
Napi::Value ConnectSignal(const Napi::CallbackInfo& info) {
|
|
211
|
+
Napi::Env env = info.Env();
|
|
212
|
+
if (info.Length() < 3 || !info[1].IsString() || !info[2].IsFunction()) {
|
|
213
|
+
Napi::TypeError::New(env, "connectSignal(handle, signalName: string, callback: function, after?: boolean)")
|
|
214
|
+
.ThrowAsJavaScriptException();
|
|
215
|
+
return env.Null();
|
|
216
|
+
}
|
|
217
|
+
GObject* obj = UnwrapGObject(env, info[0]);
|
|
218
|
+
if (obj == nullptr) return env.Null();
|
|
219
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
220
|
+
// NodeGiToBool: terminate-safe (a swallowed coercion failure must not cascade
|
|
221
|
+
// into Error::New(nullptr) — see common.h).
|
|
222
|
+
bool after = info.Length() >= 4 && NodeGiToBool(info[3]);
|
|
223
|
+
|
|
224
|
+
// Parse a possibly-detailed signal name ("notify::prop") into its signal id +
|
|
225
|
+
// detail quark, so GJS-style detailed connects work (common for notify::).
|
|
226
|
+
guint sigid = 0;
|
|
227
|
+
GQuark detail = 0;
|
|
228
|
+
if (!g_signal_parse_name(name.c_str(), G_OBJECT_TYPE(obj), &sigid, &detail, TRUE)) {
|
|
229
|
+
Napi::Error::New(env, std::string("no signal '") + name + "' on " + G_OBJECT_TYPE_NAME(obj))
|
|
230
|
+
.ThrowAsJavaScriptException();
|
|
231
|
+
return env.Null();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
JsClosureData* jc = g_new0(JsClosureData, 1);
|
|
235
|
+
jc->env = env;
|
|
236
|
+
napi_create_reference(env, info[2], 1, &jc->callback);
|
|
237
|
+
|
|
238
|
+
GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
|
|
239
|
+
g_closure_set_marshal(closure, JsClosureMarshal);
|
|
240
|
+
g_closure_add_finalize_notifier(closure, jc, JsClosureFinalize);
|
|
241
|
+
|
|
242
|
+
// g_signal_connect_closure_by_id sinks the floating closure ref + owns it,
|
|
243
|
+
// and honours the detail quark (the by-name variant cannot take a detail).
|
|
244
|
+
gulong id = g_signal_connect_closure_by_id(obj, sigid, detail, closure, after);
|
|
245
|
+
return Napi::Number::New(env, static_cast<double>(id));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// emitSignal(handle, signalName, args?) -> returnValue
|
|
249
|
+
Napi::Value EmitSignal(const Napi::CallbackInfo& info) {
|
|
250
|
+
Napi::Env env = info.Env();
|
|
251
|
+
if (info.Length() < 2 || !info[1].IsString()) {
|
|
252
|
+
Napi::TypeError::New(env, "emitSignal(handle, signalName: string, args?: unknown[])")
|
|
253
|
+
.ThrowAsJavaScriptException();
|
|
254
|
+
return env.Null();
|
|
255
|
+
}
|
|
256
|
+
GObject* obj = UnwrapGObject(env, info[0]);
|
|
257
|
+
if (obj == nullptr) return env.Null();
|
|
258
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
259
|
+
Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
|
|
260
|
+
: Napi::Array::New(env, 0);
|
|
261
|
+
|
|
262
|
+
GType gtype = G_OBJECT_TYPE(obj);
|
|
263
|
+
guint sigid = g_signal_lookup(name.c_str(), gtype);
|
|
264
|
+
if (sigid == 0) {
|
|
265
|
+
Napi::Error::New(env, std::string("no signal '") + name + "' on " + G_OBJECT_TYPE_NAME(obj))
|
|
266
|
+
.ThrowAsJavaScriptException();
|
|
267
|
+
return env.Null();
|
|
268
|
+
}
|
|
269
|
+
GSignalQuery query;
|
|
270
|
+
g_signal_query(sigid, &query);
|
|
271
|
+
|
|
272
|
+
guint n = query.n_params;
|
|
273
|
+
std::vector<GValue> params(n + 1); // [0] = instance
|
|
274
|
+
g_value_init(¶ms[0], gtype);
|
|
275
|
+
g_value_set_object(¶ms[0], obj);
|
|
276
|
+
guint initialised = 1;
|
|
277
|
+
bool ok = true;
|
|
278
|
+
for (guint i = 0; i < n; i++) {
|
|
279
|
+
GType pt = query.param_types[i] & ~G_SIGNAL_TYPE_STATIC_SCOPE;
|
|
280
|
+
g_value_init(¶ms[i + 1], pt);
|
|
281
|
+
initialised = i + 2;
|
|
282
|
+
Napi::Value v = i < args.Length() ? args.Get(i) : env.Undefined();
|
|
283
|
+
// An EMPTY v is the residue of a swallowed args.Get() failure (terminating
|
|
284
|
+
// env / throwing getter): JsToGValue's coercions on it would abort via
|
|
285
|
+
// Error::New(nullptr)'s fatal sites — bail before marshalling.
|
|
286
|
+
if (v.IsEmpty() || !JsToGValue(env, v, ¶ms[i + 1])) {
|
|
287
|
+
ok = false;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
GType rt = query.return_type & ~G_SIGNAL_TYPE_STATIC_SCOPE;
|
|
293
|
+
bool hasReturn = rt != G_TYPE_NONE && rt != G_TYPE_INVALID;
|
|
294
|
+
GValue ret = G_VALUE_INIT;
|
|
295
|
+
Napi::Value result = env.Undefined();
|
|
296
|
+
if (ok) {
|
|
297
|
+
if (hasReturn) g_value_init(&ret, rt);
|
|
298
|
+
// Mark this as a SYNCHRONOUS emit so a handler exception propagates back to
|
|
299
|
+
// this JS caller (JsClosureMarshal checks g_syncEmitDepth) rather than being
|
|
300
|
+
// surfaced + swallowed like a loop-dispatched signal.
|
|
301
|
+
g_syncEmitDepth++;
|
|
302
|
+
g_signal_emitv(params.data(), sigid, 0, hasReturn ? &ret : nullptr);
|
|
303
|
+
g_syncEmitDepth--;
|
|
304
|
+
if (hasReturn && !env.IsExceptionPending()) {
|
|
305
|
+
result = GValueToJs(env, &ret);
|
|
306
|
+
g_value_unset(&ret);
|
|
307
|
+
} else if (hasReturn) {
|
|
308
|
+
g_value_unset(&ret);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (guint j = 0; j < initialised; j++) g_value_unset(¶ms[j]);
|
|
312
|
+
return ok ? result : env.Null();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// disconnectSignal(handle, handlerId) -> void
|
|
316
|
+
Napi::Value DisconnectSignal(const Napi::CallbackInfo& info) {
|
|
317
|
+
Napi::Env env = info.Env();
|
|
318
|
+
if (info.Length() < 2 || !info[1].IsNumber()) {
|
|
319
|
+
Napi::TypeError::New(env, "disconnectSignal(handle, handlerId: number)").ThrowAsJavaScriptException();
|
|
320
|
+
return env.Undefined();
|
|
321
|
+
}
|
|
322
|
+
GObject* obj = UnwrapGObject(env, info[0]);
|
|
323
|
+
if (obj == nullptr) return env.Undefined();
|
|
324
|
+
gulong id = static_cast<gulong>(info[1].As<Napi::Number>().Int64Value());
|
|
325
|
+
if (id != 0 && g_signal_handler_is_connected(obj, id)) {
|
|
326
|
+
g_signal_handler_disconnect(obj, id);
|
|
327
|
+
}
|
|
328
|
+
return env.Undefined();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// GError domain for template-callback resolution failures surfaced to GtkBuilder.
|
|
332
|
+
static GQuark NodeGiBuilderErrorQuark() {
|
|
333
|
+
static GQuark q = g_quark_from_static_string("node-gi-builder-error");
|
|
334
|
+
return q;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- Gtk.Widget composite-template callback dispatch -----------------------
|
|
338
|
+
//
|
|
339
|
+
// Mirrors GJS's TemplateBuilderScope (refs/gjs/modules/core/overrides/Gtk.js): a
|
|
340
|
+
// custom GtkBuilderScope is set on a templated widget class via
|
|
341
|
+
// gtk_widget_class_set_template_scope, so when GtkBuilder (run by init_template)
|
|
342
|
+
// hits a `<signal name="clicked" handler="on_click"/>` it asks the scope to
|
|
343
|
+
// create a closure for the handler NAME. We resolve that name to the instance's
|
|
344
|
+
// JS method and connect a closure that dispatches to it.
|
|
345
|
+
//
|
|
346
|
+
// Why a scope (generic resolver) and not gtk_widget_class_bind_template_callback
|
|
347
|
+
// (per-name C symbol): a GtkBuilderScope's create_closure returns a *GClosure*,
|
|
348
|
+
// which is signature-agnostic — GObject marshals the signal's GValue params into
|
|
349
|
+
// it at emit time, so one mechanism handles ANY signal shape without a per-signal
|
|
350
|
+
// libffi cif. A bound C-symbol callback would instead be invoked with the live
|
|
351
|
+
// signal ABI, which we cannot know at bind time. The returned closure reuses the
|
|
352
|
+
// existing JsClosure machinery (JsClosureMarshal/JsClosureFinalize), so the marshal
|
|
353
|
+
// + GValue→JS arg conversion + lifetime are identical to a normal `.connect()`.
|
|
354
|
+
//
|
|
355
|
+
// Handler name → JS method + `this`: create_closure calls the L1 resolver
|
|
356
|
+
// (gi.js resolveTemplateCallback) with the canonical wrapper of the widget being
|
|
357
|
+
// built (gtk_builder_get_current_object) and the handler name; L1 returns the
|
|
358
|
+
// user-prototype method already bound to that instance proxy (so `this` is the
|
|
359
|
+
// template widget — the same cached, toggle-ref-canonical L1 proxy the user holds)
|
|
360
|
+
// and wrapping each native signal arg into a chainable wrapper. The returned JS
|
|
361
|
+
// function is wrapped in a JsClosure here. arg marshalling: JsClosureMarshal skips
|
|
362
|
+
// the emitter (param 0, node-gi's signal convention) and passes the rest.
|
|
363
|
+
|
|
364
|
+
// The GtkBuilderScopeInterface vtable layout (stable public ABI — replicated so the
|
|
365
|
+
// addon needs no GTK headers; it only links girepository-2.0 and dlsym's GTK).
|
|
366
|
+
typedef GType (*NodeGiScopeTypeFromNameFn)(void*, void*, const char*);
|
|
367
|
+
typedef GType (*NodeGiScopeTypeFromFuncFn)(void*, void*, const char*);
|
|
368
|
+
typedef GClosure* (*NodeGiScopeCreateClosureFn)(void*, void*, const char*, guint, GObject*,
|
|
369
|
+
GError**);
|
|
370
|
+
struct NodeGiBuilderScopeIface {
|
|
371
|
+
GTypeInterface g_iface;
|
|
372
|
+
NodeGiScopeTypeFromNameFn get_type_from_name;
|
|
373
|
+
NodeGiScopeTypeFromFuncFn get_type_from_function;
|
|
374
|
+
NodeGiScopeCreateClosureFn create_closure;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// create_closure: resolve `function_name` to the widget instance's JS method and
|
|
378
|
+
// return a JsClosure dispatching to it. Returns NULL + sets *error on any failure
|
|
379
|
+
// (handler not found, swapped flag, no env/resolver) so GtkBuilder aborts the build
|
|
380
|
+
// with a clear message instead of silently dropping the handler.
|
|
381
|
+
static GClosure* NodeGiScopeCreateClosure(void* selfScope, void* builder,
|
|
382
|
+
const char* function_name, guint flags,
|
|
383
|
+
GObject* object, GError** error) {
|
|
384
|
+
const char* name = function_name != nullptr ? function_name : "?";
|
|
385
|
+
// SWAPPED is unsupported (matches GJS's "_createClosure" guard).
|
|
386
|
+
if (flags & 1u /* GTK_BUILDER_CLOSURE_SWAPPED */) {
|
|
387
|
+
g_set_error(error, NodeGiBuilderErrorQuark(), 0,
|
|
388
|
+
"node-gi: template signal flag 'swapped' is not supported (handler '%s')", name);
|
|
389
|
+
return nullptr;
|
|
390
|
+
}
|
|
391
|
+
napi_env rawEnv = static_cast<napi_env>(
|
|
392
|
+
g_object_get_qdata(G_OBJECT(selfScope), NodeGiScopeEnvQuark()));
|
|
393
|
+
if (rawEnv == nullptr) {
|
|
394
|
+
g_set_error(error, NodeGiBuilderErrorQuark(), 0,
|
|
395
|
+
"node-gi: template scope has no live JS env to resolve handler '%s'", name);
|
|
396
|
+
return nullptr;
|
|
397
|
+
}
|
|
398
|
+
const GtkTemplateApi* gtk = GetGtkTemplateApi();
|
|
399
|
+
GObject* thisObj = object;
|
|
400
|
+
if (thisObj == nullptr && gtk->builder_get_current_object != nullptr)
|
|
401
|
+
thisObj = gtk->builder_get_current_object(builder);
|
|
402
|
+
if (thisObj == nullptr) {
|
|
403
|
+
g_set_error(error, NodeGiBuilderErrorQuark(), 0,
|
|
404
|
+
"node-gi: no current object to bind template handler '%s'", name);
|
|
405
|
+
return nullptr;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
Napi::Env env(rawEnv);
|
|
409
|
+
Napi::HandleScope hscope(env);
|
|
410
|
+
|
|
411
|
+
NodeGiEnvData* d = EnvData(rawEnv);
|
|
412
|
+
napi_value resolver = nullptr;
|
|
413
|
+
if (d == nullptr || d->templateCallbackResolver == nullptr ||
|
|
414
|
+
napi_get_reference_value(rawEnv, d->templateCallbackResolver, &resolver) != napi_ok ||
|
|
415
|
+
resolver == nullptr) {
|
|
416
|
+
g_set_error(error, NodeGiBuilderErrorQuark(), 0,
|
|
417
|
+
"node-gi: no template-callback resolver registered (handler '%s')", name);
|
|
418
|
+
return nullptr;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
Napi::Value handleVal = WrapGObject(env, thisObj, GI_TRANSFER_NOTHING);
|
|
422
|
+
napi_value args[2] = {handleVal, nullptr};
|
|
423
|
+
napi_create_string_utf8(rawEnv, name, NAPI_AUTO_LENGTH, &args[1]);
|
|
424
|
+
napi_value undef = nullptr;
|
|
425
|
+
napi_get_undefined(rawEnv, &undef);
|
|
426
|
+
napi_value resolved = nullptr;
|
|
427
|
+
napi_status st = napi_call_function(rawEnv, undef, resolver, 2, args, &resolved);
|
|
428
|
+
if (st != napi_ok) {
|
|
429
|
+
// The resolver threw — never leave a pending JS exception across the return to
|
|
430
|
+
// GTK (C). Clear it and fold its message into the GError so the build fails
|
|
431
|
+
// cleanly.
|
|
432
|
+
napi_value ex = nullptr;
|
|
433
|
+
std::string detail;
|
|
434
|
+
if (napi_get_and_clear_last_exception(rawEnv, &ex) == napi_ok && ex != nullptr) {
|
|
435
|
+
napi_value msg = nullptr;
|
|
436
|
+
if (napi_get_named_property(rawEnv, ex, "message", &msg) == napi_ok) {
|
|
437
|
+
size_t len = 0;
|
|
438
|
+
if (napi_get_value_string_utf8(rawEnv, msg, nullptr, 0, &len) == napi_ok) {
|
|
439
|
+
detail.resize(len);
|
|
440
|
+
napi_get_value_string_utf8(rawEnv, msg, detail.data(), len + 1, &len);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
g_set_error(error, NodeGiBuilderErrorQuark(), 0,
|
|
445
|
+
"node-gi: template handler '%s' resolver threw%s%s", name,
|
|
446
|
+
detail.empty() ? "" : ": ", detail.c_str());
|
|
447
|
+
return nullptr;
|
|
448
|
+
}
|
|
449
|
+
napi_valuetype rt = napi_undefined;
|
|
450
|
+
napi_typeof(rawEnv, resolved, &rt);
|
|
451
|
+
if (rt != napi_function) {
|
|
452
|
+
g_set_error(error, NodeGiBuilderErrorQuark(), 0,
|
|
453
|
+
"node-gi: no template handler '%s' defined on the instance", name);
|
|
454
|
+
return nullptr;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Wrap the resolved (instance-bound, arg-wrapping) JS function in a JsClosure —
|
|
458
|
+
// the same closure type a `.connect()` produces. The closure owns a strong ref to
|
|
459
|
+
// the function; g_object_watch_closure ties its lifetime to the widget (the
|
|
460
|
+
// handler `this`) and invalidates it when the widget is finalized, exactly like
|
|
461
|
+
// GtkBuilderCScope's own create_closure.
|
|
462
|
+
JsClosureData* jc = g_new0(JsClosureData, 1);
|
|
463
|
+
jc->env = rawEnv;
|
|
464
|
+
napi_create_reference(rawEnv, resolved, 1, &jc->callback);
|
|
465
|
+
GClosure* closure = g_closure_new_simple(sizeof(GClosure), jc);
|
|
466
|
+
g_closure_set_marshal(closure, JsClosureMarshal);
|
|
467
|
+
g_closure_add_finalize_notifier(closure, jc, JsClosureFinalize);
|
|
468
|
+
g_object_watch_closure(thisObj, closure);
|
|
469
|
+
return closure;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
static void NodeGiBuilderScopeIfaceInit(gpointer g_iface, gpointer /*data*/) {
|
|
473
|
+
// The per-type interface vtable is pre-filled with the GtkBuilderScope interface
|
|
474
|
+
// defaults (get_type_from_name = g_type_from_name, etc.) before this runs, so we
|
|
475
|
+
// only override create_closure — type resolution keeps GTK's default behaviour
|
|
476
|
+
// (sufficient for templates whose object types are already registered).
|
|
477
|
+
NodeGiBuilderScopeIface* iface = static_cast<NodeGiBuilderScopeIface*>(g_iface);
|
|
478
|
+
iface->create_closure = NodeGiScopeCreateClosure;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// The GObject subtype implementing GtkBuilderScope (registered once). Returns 0 if
|
|
482
|
+
// the GTK scope API is unavailable (old/missing GTK) → callers skip the scope.
|
|
483
|
+
static GType NodeGiBuilderScopeGetType() {
|
|
484
|
+
static GType type = 0;
|
|
485
|
+
if (type != 0) return type;
|
|
486
|
+
const GtkTemplateApi* gtk = GetGtkTemplateApi();
|
|
487
|
+
if (gtk->builder_scope_get_type == nullptr || gtk->set_template_scope == nullptr) return 0;
|
|
488
|
+
GType ifaceType = gtk->builder_scope_get_type();
|
|
489
|
+
if (ifaceType == 0) return 0;
|
|
490
|
+
|
|
491
|
+
GTypeInfo info = {};
|
|
492
|
+
info.class_size = sizeof(GObjectClass);
|
|
493
|
+
info.instance_size = sizeof(GObject);
|
|
494
|
+
type = g_type_register_static(G_TYPE_OBJECT, "NodeGiBuilderScope", &info,
|
|
495
|
+
static_cast<GTypeFlags>(0));
|
|
496
|
+
if (type == 0) return 0;
|
|
497
|
+
GInterfaceInfo ifaceInfo = {NodeGiBuilderScopeIfaceInit, nullptr, nullptr};
|
|
498
|
+
g_type_add_interface_static(type, ifaceType, &ifaceInfo);
|
|
499
|
+
return type;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Create the class's template-callback scope and install it (class_init). Holds the
|
|
503
|
+
// scope for the class lifetime (process-permanent, like NodeGiClassData) so its env
|
|
504
|
+
// qdata can be refreshed per construction. A no-op when the GTK scope API is absent.
|
|
505
|
+
void NodeGiInstallTemplateScopeOnClass(NodeGiClassData* cd, gpointer g_class) {
|
|
506
|
+
const GtkTemplateApi* gtk = GetGtkTemplateApi();
|
|
507
|
+
if (gtk->set_template_scope == nullptr) return; // older GTK: callbacks unsupported
|
|
508
|
+
GType scopeType = NodeGiBuilderScopeGetType();
|
|
509
|
+
if (scopeType == 0) return;
|
|
510
|
+
GObject* scope = static_cast<GObject*>(g_object_new(scopeType, nullptr));
|
|
511
|
+
if (scope == nullptr) return;
|
|
512
|
+
gtk->set_template_scope(g_class, scope); // the class takes its own ref
|
|
513
|
+
cd->templateScope = scope; // our class-lifetime ref (env-refresh target)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// setTemplateCallbackResolver(fn) -> void. L1 registers the resolver that maps a
|
|
517
|
+
// (instanceHandle, handlerName) to the instance's bound JS method (see gi.js).
|
|
518
|
+
Napi::Value SetTemplateCallbackResolver(const Napi::CallbackInfo& info) {
|
|
519
|
+
Napi::Env env = info.Env();
|
|
520
|
+
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
521
|
+
Napi::TypeError::New(env, "setTemplateCallbackResolver(resolver: function)")
|
|
522
|
+
.ThrowAsJavaScriptException();
|
|
523
|
+
return env.Undefined();
|
|
524
|
+
}
|
|
525
|
+
NodeGiEnvData* d = EnvData(env);
|
|
526
|
+
if (d == nullptr) return env.Undefined();
|
|
527
|
+
if (d->templateCallbackResolver != nullptr) {
|
|
528
|
+
napi_delete_reference(env, d->templateCallbackResolver);
|
|
529
|
+
d->templateCallbackResolver = nullptr;
|
|
530
|
+
}
|
|
531
|
+
napi_create_reference(env, info[0], 1, &d->templateCallbackResolver);
|
|
532
|
+
return env.Undefined();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
} // namespace nodegi
|
package/src/template.cc
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Gtk.Widget composite-template API via dlsym: template init + template children.
|
|
3
|
+
|
|
4
|
+
#include "common.h"
|
|
5
|
+
|
|
6
|
+
namespace nodegi {
|
|
7
|
+
|
|
8
|
+
// Resolve the GTK template API once (C++11 function-local static init is
|
|
9
|
+
// thread-safe; node-gi calls these only on the main thread). dlopen with
|
|
10
|
+
// RTLD_NOLOAD first — requireGi('Gtk','4.0') already dlopened libgtk-4 via the
|
|
11
|
+
// typelib, so this just bumps the refcount; fall back to a plain dlopen so a
|
|
12
|
+
// caller that never required Gtk through the typelib still resolves the symbols.
|
|
13
|
+
const GtkTemplateApi* GetGtkTemplateApi() {
|
|
14
|
+
static GtkTemplateApi api = {};
|
|
15
|
+
static bool initialised = false;
|
|
16
|
+
if (initialised) return &api;
|
|
17
|
+
initialised = true;
|
|
18
|
+
#ifdef _WIN32
|
|
19
|
+
// The Gtk.Widget composite-template API is resolved lazily via dlsym on POSIX
|
|
20
|
+
// only. GTK on Windows is Phase 2; the Phase-1 Windows CI proves the display-
|
|
21
|
+
// free core, which never exercises templates. api.ok stays false → template
|
|
22
|
+
// callers warn + no-op / throw a clear "template API unavailable" error.
|
|
23
|
+
return &api;
|
|
24
|
+
#else
|
|
25
|
+
void* lib = dlopen("libgtk-4.so.1", RTLD_LAZY | RTLD_NOLOAD);
|
|
26
|
+
if (lib == nullptr) lib = dlopen("libgtk-4.so.1", RTLD_LAZY);
|
|
27
|
+
if (lib == nullptr) return &api; // api.ok stays false → callers warn + no-op
|
|
28
|
+
api.set_template = reinterpret_cast<decltype(api.set_template)>(
|
|
29
|
+
dlsym(lib, "gtk_widget_class_set_template"));
|
|
30
|
+
api.set_template_from_resource = reinterpret_cast<decltype(api.set_template_from_resource)>(
|
|
31
|
+
dlsym(lib, "gtk_widget_class_set_template_from_resource"));
|
|
32
|
+
api.bind_template_child_full = reinterpret_cast<decltype(api.bind_template_child_full)>(
|
|
33
|
+
dlsym(lib, "gtk_widget_class_bind_template_child_full"));
|
|
34
|
+
api.set_css_name =
|
|
35
|
+
reinterpret_cast<decltype(api.set_css_name)>(dlsym(lib, "gtk_widget_class_set_css_name"));
|
|
36
|
+
api.init_template =
|
|
37
|
+
reinterpret_cast<decltype(api.init_template)>(dlsym(lib, "gtk_widget_init_template"));
|
|
38
|
+
api.get_template_child = reinterpret_cast<decltype(api.get_template_child)>(
|
|
39
|
+
dlsym(lib, "gtk_widget_get_template_child"));
|
|
40
|
+
api.set_template_scope = reinterpret_cast<decltype(api.set_template_scope)>(
|
|
41
|
+
dlsym(lib, "gtk_widget_class_set_template_scope"));
|
|
42
|
+
api.builder_get_current_object = reinterpret_cast<decltype(api.builder_get_current_object)>(
|
|
43
|
+
dlsym(lib, "gtk_builder_get_current_object"));
|
|
44
|
+
api.builder_scope_get_type = reinterpret_cast<decltype(api.builder_scope_get_type)>(
|
|
45
|
+
dlsym(lib, "gtk_builder_scope_get_type"));
|
|
46
|
+
api.ok = api.set_template != nullptr && api.set_template_from_resource != nullptr &&
|
|
47
|
+
api.bind_template_child_full != nullptr && api.set_css_name != nullptr &&
|
|
48
|
+
api.init_template != nullptr && api.get_template_child != nullptr;
|
|
49
|
+
return &api;
|
|
50
|
+
#endif // _WIN32
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The live JS env stamped on a template-callback scope (refreshed each construct).
|
|
54
|
+
GQuark NodeGiScopeEnvQuark() {
|
|
55
|
+
static GQuark q = g_quark_from_static_string("node-gi-scope-env");
|
|
56
|
+
return q;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Instantiate the Gtk.Widget template on a freshly-constructed instance (see the
|
|
60
|
+
// forward declaration above ConstructGObject). A no-op unless the instance's
|
|
61
|
+
// registered type carries node-gi template data, so it is safe on every GObject
|
|
62
|
+
// construction. Uses the instance's actual type's class data (single-level
|
|
63
|
+
// registered templated type — the construct() case).
|
|
64
|
+
void MaybeInitTemplate(Napi::Env env, GObject* obj) {
|
|
65
|
+
NodeGiClassData* cd = FindClassData(G_OBJECT_TYPE(obj));
|
|
66
|
+
if (cd == nullptr || !cd->hasTemplate) return;
|
|
67
|
+
const GtkTemplateApi* gtk = GetGtkTemplateApi();
|
|
68
|
+
if (!gtk->ok) return;
|
|
69
|
+
// Only a Gtk.Widget can be init_template'd (class_init also skips a non-widget
|
|
70
|
+
// template). g_type_from_name is 0 if GTK never loaded → guard is false → skip.
|
|
71
|
+
GType widgetType = g_type_from_name("GtkWidget");
|
|
72
|
+
if (widgetType == 0 || !g_type_is_a(G_OBJECT_TYPE(obj), widgetType)) return;
|
|
73
|
+
// Stamp the live JS env on the template-callback scope BEFORE init_template runs:
|
|
74
|
+
// GtkBuilder resolves every `<signal handler="…">` synchronously inside
|
|
75
|
+
// init_template by calling the scope's create_closure, which needs this env to
|
|
76
|
+
// call back into the L1 resolver. Refreshed each construct so the scope always
|
|
77
|
+
// dispatches in the env that built this instance (GTK template construction is
|
|
78
|
+
// main-thread/single-env; a stale capture would only matter for a cross-env
|
|
79
|
+
// build, which GTK does not do).
|
|
80
|
+
if (cd->templateScope != nullptr) {
|
|
81
|
+
g_object_set_qdata(cd->templateScope, NodeGiScopeEnvQuark(),
|
|
82
|
+
reinterpret_cast<void*>(static_cast<napi_env>(env)));
|
|
83
|
+
}
|
|
84
|
+
gtk->init_template(obj);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// getTemplateChild(handle, name) -> wrapped child GObject | null
|
|
88
|
+
//
|
|
89
|
+
// Resolve a composite-template child bound on the instance's type (declared via
|
|
90
|
+
// registerClass Children/InternalChildren) by name. Returns the child wrapped
|
|
91
|
+
// through the canonical toggle-ref bridge (GI_TRANSFER_NOTHING — the child is
|
|
92
|
+
// owned by the parent widget via the template, a borrowed pointer). The L1 layer
|
|
93
|
+
// assigns the result onto the instance (public `this.name`, internal `this._name`).
|
|
94
|
+
Napi::Value GetTemplateChild(const Napi::CallbackInfo& info) {
|
|
95
|
+
Napi::Env env = info.Env();
|
|
96
|
+
if (info.Length() < 2 || !info[1].IsString()) {
|
|
97
|
+
Napi::TypeError::New(env, "getTemplateChild(handle, name: string)").ThrowAsJavaScriptException();
|
|
98
|
+
return env.Null();
|
|
99
|
+
}
|
|
100
|
+
GObject* obj = UnwrapGObject(env, info[0]);
|
|
101
|
+
if (obj == nullptr) return env.Null();
|
|
102
|
+
std::string name = info[1].As<Napi::String>().Utf8Value();
|
|
103
|
+
const GtkTemplateApi* gtk = GetGtkTemplateApi();
|
|
104
|
+
if (!gtk->ok) {
|
|
105
|
+
Napi::Error::New(env, "node-gi: the libgtk-4 template API is unavailable")
|
|
106
|
+
.ThrowAsJavaScriptException();
|
|
107
|
+
return env.Null();
|
|
108
|
+
}
|
|
109
|
+
// Look the child up against the instance's actual type (the registered
|
|
110
|
+
// templated type for the single-level case the decorator constructs).
|
|
111
|
+
GObject* child = gtk->get_template_child(obj, G_OBJECT_TYPE(obj), name.c_str());
|
|
112
|
+
if (child == nullptr) return env.Null();
|
|
113
|
+
return WrapGObject(env, child, GI_TRANSFER_NOTHING);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
} // namespace nodegi
|