node-gtk 2.1.0 → 3.0.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 CHANGED
@@ -2,7 +2,7 @@
2
2
  <a>
3
3
  <img
4
4
  alt="NODE-GTK"
5
- width="250"
5
+ width="200"
6
6
  src="https://raw.githubusercontent.com/romgrk/node-gtk/master/img/node-gtk-logo.svg?sanitize=true"
7
7
  />
8
8
  </a>
@@ -10,191 +10,91 @@
10
10
 
11
11
  <h1 align="center">node-gtk</h1>
12
12
  <p align="center">
13
- <b>GNOME Gtk+ bindings for NodeJS</b>
13
+ <b>GTK bindings for NodeJS</b>
14
14
  <br/>
15
- <img src="https://img.shields.io/npm/v/node-gtk" alt="Package Version" />
16
15
  </p>
17
16
 
18
- Node-Gtk is a [gobject-introspection](https://gi.readthedocs.io/en/latest) library for nodejs. It makes it possible to
19
- use any introspected library, such as Gtk+, usable. It is similar in essence to [GJS](https://wiki.gnome.org/action/show/Projects/Gjs) or [PyGObject](https://pygobject.readthedocs.io). Please note this project is currently in a _beta_ state and is being developed. Any contributors willing to help
20
- will be welcomed.
21
-
22
- Supported Node.js versions: **20**, **22**, **24** (other versions may work but are untested)<br>
23
- Pre-built binaries available for: **Linux**, **macOS**
24
-
25
- ### Table of contents
26
-
27
- - [Usage](#usage)
28
- - [Documentation](#documentation)
29
- - [TypeScript](#typescript)
30
- - [Installing and building](#installing-and-building)
31
- - [Contributing](#contributing)
32
-
33
- ## Usage
34
-
35
- Below is a [minimal example](./examples/hello-world.js) of how to use node-gtk:
36
-
37
- ```javascript
38
- const gi = require('node-gtk');
39
- const GLib = gi.require('GLib', '2.0');
40
- const Gtk = gi.require('Gtk', '4.0');
41
- const Adw = gi.require('Adw', '1');
42
-
43
- const loop = GLib.MainLoop.new(null, false);
44
- const app = new Adw.Application('com.github.romgrk.node-gtk.hello', 0);
45
-
46
- app.on('activate', () => {
47
- const content = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL });
48
- content.append(new Adw.HeaderBar());
49
- content.append(new Gtk.Label({ label: 'Hello Adwaita!', vexpand: true }));
17
+ <p align="center">
18
+ <a href="#usage">Usage</a> · <a href="#installing">Installing</a> · <a href="./doc/index.md">Documentation</a> · <a href="#contributing">Contributing</a>
19
+ </p>
50
20
 
51
- const window = new Adw.ApplicationWindow(app);
52
- window.setTitle('node-gtk');
53
- window.setDefaultSize(300, 120);
54
- window.setContent(content);
55
- window.on('close-request', () => (loop.quit(), app.quit(), false));
56
- window.present();
21
+ <br />
57
22
 
58
- gi.startLoop();
59
- loop.run();
60
- });
61
-
62
- process.exit(app.run([]));
63
- ```
23
+ `node-gtk` lets you build native GTK apps on **linux**, **macOS** and **windows**
24
+ with full **ESM**, **TypeScript** and **CSS hot-reload** support. Prebuilt binaries
25
+ are available for Node.js versions **20**, **22** and **24**.
64
26
 
65
27
  <p align="center">
66
- <img src="./img/hello-world.png" style="width: 290px; height: auto;"/>
28
+ <img src="./img/browser.png" style="max-width: 500px; height: auto;" alt="A web browser build with node-gtk" />
67
29
  </p>
68
30
 
69
- #### ES modules
70
-
71
- The example above is CommonJS. node-gtk also works under ES modules, but with one
72
- behavioral difference around the blocking main-loop calls (`GLib.MainLoop.run`,
73
- `Gio`/`Gtk.Application.run`, `Gtk.main`).
74
-
75
- Under ESM, a module's top-level body runs as a V8 *microtask*. If a blocking
76
- loop call ran synchronously from there, it would nest inside V8's microtask
77
- drain and starve every `Promise`/`async` continuation (so `await fetch(...)`,
78
- `fs/promises`, etc. would never settle) for the entire lifetime of the loop —
79
- see [#442](https://github.com/romgrk/node-gtk/issues/442). To avoid this,
80
- node-gtk defers the blocking call to the next event-loop tick when it detects it
81
- is being invoked from within a microtask. Two consequences for ESM code:
82
-
83
- - The run call **returns immediately** instead of blocking until quit, so any
84
- code placed *after* it executes *before* the loop. Make the run call the last
85
- statement, and do cleanup/exit from your quit/`close-request` handler.
86
- - Its **return value is not available** (e.g. the application's exit status), so
87
- don't wrap it like `process.exit(app.run([]))` — call `app.run([])` on its own
88
- and exit from the handler instead.
89
-
90
- Under CommonJS (and inside signal callbacks) nothing changes: the run calls block
91
- synchronously and return their value exactly as before.
92
-
93
- > **Design note (may be reconsidered).** Two approaches were considered for #442:
94
- >
95
- > - **Transparent auto-defer (chosen).** node-gtk detects the microtask context
96
- > and defers the blocking call automatically, so existing ESM code keeps
97
- > working with no changes. The cost is the leaky behavior above: under ESM the
98
- > run call no longer blocks, which is surprising and silently breaks idioms
99
- > like `process.exit(app.run([]))`.
100
- > - **Explicit async API (alternative).** Keep the blocking calls strictly
101
- > synchronous and add awaitable variants (e.g. `await loop.runAsync()`),
102
- > leaving ESM users to opt in. This has clean, predictable semantics and no
103
- > hidden return-immediately behavior, but it does *not* transparently fix
104
- > existing ESM code — users must change their code, and plain `loop.run()`
105
- > would still starve microtasks under ESM (or would need to throw/warn).
106
- >
107
- > The transparent approach was chosen to fix existing code out of the box, but
108
- > given the leaky semantics this trade-off may be revisited — possibly moving to
109
- > (or also offering) the explicit async API.
110
-
111
- You can also easily create custom applications:
112
-
113
- [A web browser (using WebKit2GTK)](./examples/browser.js)
31
+ ## Usage
114
32
 
115
- <p align="center">
116
- <img src="./img/browser.png" style="max-width: 500px; height: auto;"/>
117
- </p>
33
+ The **create** tool generates a complete, ready-to-run GTK/Adwaita project, so you
34
+ can start building immediately after [installing GTK4](#installing):
118
35
 
119
- [A system monitor](./examples/system-monitor.js)
36
+ ```sh
37
+ npx node-gtk create <your-app>
38
+ ```
120
39
 
121
40
  <p align="center">
122
- <img src="./img/system-monitor.png" style="width: 400px; height: auto;"/>
41
+ <img src="./img/create-app-example.png" style="width: 500px; height: auto;"/>
123
42
  </p>
124
43
 
125
- #### Other projects
44
+ Also see our [hello world](./examples/hello-world.mjs), [web browser](./examples/browser.mjs)
45
+ or [system monitor](./examples/system-monitor.mjs) examples.
126
46
 
127
- The [react-gtk](https://github.com/codejamninja/react-gtk) project may also allow you to use GTK via React (unmaintained).
47
+ ## Installing
128
48
 
129
- ## Documentation
49
+ There are two steps:
130
50
 
131
- [Read our documentation here](./doc/index.md)
132
-
133
- ## TypeScript
51
+ 1. Install `node-gtk` itself (*done by the create tool*)
52
+ 2. Install the native libraries you use (see examples per platform below)
134
53
 
135
- node-gtk can generate TypeScript declarations for the libraries you use,
136
- straight from the GObject-Introspection typelibs installed on your machine — so
137
- the types always match your actual library versions and node-gtk's own runtime
138
- shape (camelCase methods, signal callbacks, nullability, etc.).
54
+ #### Linux
139
55
 
140
56
  ```sh
141
- # generates ./node_modules/.node-gtk-types (a hidden, git-ignored cache)
142
- npx node-gtk generate-types Gtk-4.0 Adw-1
143
- ```
57
+ # archlinux
58
+ pacman -S gtk4 libadwaita
144
59
 
145
- The command emits one declaration file per namespace (plus the full dependency
146
- closure) and a `node-gtk.d.ts` shim. Point your `tsconfig.json` at it:
60
+ # fedora
61
+ dnf install gtk4 libadwaita
147
62
 
148
- ```jsonc
149
- {
150
- "compilerOptions": {
151
- "moduleResolution": "node16",
152
- "paths": { "node-gtk": ["./node_modules/.node-gtk-types/node-gtk.d.ts"] }
153
- }
154
- }
63
+ # ubuntu
64
+ # Already installed :)
155
65
  ```
156
66
 
157
- Then `gi.require` is fully typed — the namespace is inferred from the string
158
- arguments:
67
+ #### macOS
159
68
 
160
- ```ts
161
- import * as gi from 'node-gtk'
162
-
163
- const Gtk = gi.require('Gtk', '4.0') // typed as the Gtk-4.0 namespace
164
- const win = new Gtk.ApplicationWindow({ title: 'Hello', defaultWidth: 400 })
165
- win.on('close-request', () => false) // signal name + callback are typed
69
+ ```sh
70
+ brew install gtk4 libadwaita adwaita-icon-theme
166
71
  ```
167
72
 
168
- You get typed constructor properties (including inherited and interface ones),
169
- camelCase methods with real return types, GI nullability, typed signal
170
- overloads, enums, `bigint` for 64-bit integers, out-parameters surfaced as the
171
- return value, and cross-namespace types. GNOME's API documentation is included
172
- as JSDoc (with `@param`/`@returns`), so editors show it on hover — this reads the
173
- `.gir` files installed by the libraries' `-dev`/`-devel` packages; pass
174
- `--no-docs` for leaner output if they aren't installed or you don't want them.
73
+ #### Windows
175
74
 
176
- Because the output is a generated cache under `node_modules`, add a `postinstall`
177
- script so it regenerates on install:
178
-
179
- ```json
180
- { "scripts": { "postinstall": "node-gtk generate-types Gtk-4.0 Adw-1" } }
75
+ ```sh
76
+ # Already installed :)
181
77
  ```
182
78
 
183
- Run `npx node-gtk generate-types --help` for options.
79
+ > [!NOTE]
80
+ > Windows doesn't have the dependencies we need in a package manager, therefore
81
+ > `node-gtk` ships prebuilt versions of GTK 4 / Adwaita, so `npm install node-gtk`
82
+ > is all you need **if** your dependency is in our
83
+ > [list of prebuilt libraries](./windows/runtime-libraries.txt).
184
84
 
185
- ## Installing and building
85
+ ### build from source
186
86
 
187
- See [Installing & building](./doc/installation.md) for prebuilt-binary notes, per-platform build instructions (Linux, macOS, Windows), and how to run the tests and examples.
87
+ Building from source, or [contributing](./doc/contributing.md)? See [Building from source](./doc/building.md).
188
88
 
189
- ## Contributing
89
+ ## Documentation
90
+
91
+ [Read our documentation here](./doc/index.md)
190
92
 
191
- If you'd like to help, we'd be more than happy to have support. To setup your development environment, you can
192
- run `npm run configure`. You can then build the project with `npm run build`. To generate the `compile_commands.json`
193
- for LSP to work nicely, you can use [bear](https://github.com/rizsotto/Bear) as `bear -- npm run build`.
93
+ ## Other notes
194
94
 
195
- - https://developer.gnome.org/gi/stable/index.html
196
- - https://v8docs.nodesource.com/
197
- - https://github.com/nodejs/nan#api
95
+ `node-gtk` is a [gobject-introspection](https://gi.readthedocs.io/en/latest) library
96
+ for nodejs. It makes it possible to use any introspected C library, such as GTK,
97
+ usable. It is similar in essence to [GJS](https://wiki.gnome.org/action/show/Projects/Gjs)
98
+ or [PyGObject](https://pygobject.readthedocs.io).
198
99
 
199
- There is a [Discord channel](https://discord.gg/r2VqPUV) but it receives little monitoring, use github issues or
200
- discussions preferably.
100
+ [MIT License](./LICENSE)
package/bin/node-gtk.js CHANGED
@@ -4,6 +4,8 @@
4
4
  *
5
5
  * Subcommands:
6
6
  * generate-types Generate TypeScript declarations from the installed typelibs.
7
+ * create Create a new GTK/Adwaita application.
8
+ * list List the GObject-Introspection libraries available locally.
7
9
  */
8
10
 
9
11
  const cmd = process.argv[2]
@@ -12,6 +14,13 @@ switch (cmd) {
12
14
  case 'generate-types':
13
15
  require('../tools/generate-types.js').run(process.argv.slice(3))
14
16
  break
17
+ case 'create':
18
+ require('../tools/create-app.js').run(process.argv.slice(3))
19
+ break
20
+ case 'list':
21
+ case 'list-libraries':
22
+ require('../tools/list-libraries.js').run(process.argv.slice(3))
23
+ break
15
24
  case undefined:
16
25
  case '-h':
17
26
  case '--help':
@@ -20,9 +29,11 @@ switch (cmd) {
20
29
  Usage: node-gtk <command> [options]
21
30
 
22
31
  Commands:
32
+ create <directory> Create a new GTK/Adwaita app
23
33
  generate-types <Namespace-Version> [...] Generate TypeScript types (.d.ts)
34
+ list [filter] List available libraries & versions
24
35
 
25
- Run \`node-gtk generate-types --help\` for details.`)
36
+ Run \`node-gtk <command> --help\` for details.`)
26
37
  process.exit(cmd ? 0 : 1)
27
38
  break
28
39
  default:
@@ -0,0 +1,49 @@
1
+ /*
2
+ * hooks.mjs — Node.js ESM module-customization hooks for the `gi:` scheme.
3
+ *
4
+ * Enables `import Gtk from 'gi:Gtk-4.0'`, where the default export is the
5
+ * namespace object returned by node-gtk's `gi.require('Gtk', '4.0')`. Members are
6
+ * read off that object: `const { Box, Label } = Gtk`.
7
+ *
8
+ * Install them with `node --import node-gtk/register app.mjs` (see register.mjs).
9
+ *
10
+ * The hooks run on a separate loader thread, so they do NO native work: `load`
11
+ * only emits a tiny synthetic ES module whose body calls `gi.require` on the
12
+ * main thread when the module is evaluated. Requires Node >= 20.6 (module.register).
13
+ */
14
+
15
+ const PREFIX = 'gi:'
16
+
17
+ /* Absolute file:// URL to lib/index.js, embedded into the generated source. The
18
+ * synthetic module's parent URL is the schemeless `gi:` URL, which has no
19
+ * filesystem base, so a bare `import 'node-gtk'` could not be resolved from it —
20
+ * the absolute URL sidesteps resolution entirely. */
21
+ const INDEX_URL = new URL('../index.js', import.meta.url).href
22
+
23
+ export async function resolve(specifier, context, nextResolve) {
24
+ if (specifier.startsWith(PREFIX))
25
+ return { url: specifier, shortCircuit: true }
26
+ return nextResolve(specifier, context)
27
+ }
28
+
29
+ export async function load(url, context, nextLoad) {
30
+ if (!url.startsWith(PREFIX))
31
+ return nextLoad(url, context)
32
+
33
+ // `gi:Gtk-4.0` -> ('Gtk', '4.0'); `gi:Adw-1` -> ('Adw', '1'); `gi:cairo` -> ('cairo', null).
34
+ // Split on the first '-' only: GI namespace names never contain '-', versions may.
35
+ const spec = url.slice(PREFIX.length)
36
+ const dash = spec.indexOf('-')
37
+ const name = dash === -1 ? spec : spec.slice(0, dash)
38
+ const version = dash === -1 ? null : spec.slice(dash + 1)
39
+
40
+ const call = version === null
41
+ ? `gi.require(${JSON.stringify(name)})`
42
+ : `gi.require(${JSON.stringify(name)}, ${JSON.stringify(version)})`
43
+
44
+ const source =
45
+ `import gi from ${JSON.stringify(INDEX_URL)};\n` +
46
+ `export default ${call};\n`
47
+
48
+ return { format: 'module', shortCircuit: true, source }
49
+ }
@@ -0,0 +1,17 @@
1
+ /*
2
+ * register.mjs — install the `gi:` import hooks.
3
+ *
4
+ * Usage: node --import node-gtk/register app.mjs
5
+ *
6
+ * Then, in app.mjs:
7
+ * import Gtk from 'gi:Gtk-4.0'
8
+ * const { Box, Label } = Gtk
9
+ *
10
+ * Note: hooks only affect imports evaluated *after* registration. To use a static
11
+ * `import ... from 'gi:...'` in your entry module, register via the `--import`
12
+ * flag above (not a programmatic `import 'node-gtk/register'` in that same file).
13
+ */
14
+
15
+ import { register } from 'node:module'
16
+
17
+ register(new URL('./hooks.mjs', import.meta.url))
package/lib/index.js CHANGED
@@ -8,7 +8,7 @@ const moduleCache = internal.GetModuleCache()
8
8
  // Must be loaded first, to setup the GI functions
9
9
  const bootstrap = require('./bootstrap.js')
10
10
  const module_ = require('./module.js')
11
- const loop = require('./loop.js')
11
+ require('./loop.js') // installs the automatic main-loop integration
12
12
  const registerClass = require('./register-class.js')
13
13
 
14
14
  /**
@@ -42,7 +42,6 @@ function getGType(value) {
42
42
  module.exports = {
43
43
  // Public API
44
44
  ...module_,
45
- startLoop: loop.start,
46
45
  registerClass: registerClass,
47
46
  getGType: getGType,
48
47
  System: internal.System,
package/lib/index.mjs ADDED
@@ -0,0 +1,25 @@
1
+ /*
2
+ * index.mjs
3
+ *
4
+ * ESM facade over the CommonJS entry (index.js), so that named imports work:
5
+ *
6
+ * import gi, { require, registerClass } from 'node-gtk'
7
+ *
8
+ * Node's CommonJS-to-ESM named-export detection (cjs-module-lexer) cannot see
9
+ * through index.js's computed `module.exports`, so we re-export explicitly here.
10
+ * Both this file and `require('node-gtk')` share the same underlying index.js
11
+ * instance (Node caches it by path), so there is no duplicated state.
12
+ */
13
+
14
+ import gi from './index.js'
15
+
16
+ export default gi
17
+
18
+ export const require = gi.require
19
+ export const isLoaded = gi.isLoaded
20
+ export const prependSearchPath = gi.prependSearchPath
21
+ export const prependLibraryPath = gi.prependLibraryPath
22
+ export const listAvailableModules = gi.listAvailableModules
23
+ export const registerClass = gi.registerClass
24
+ export const getGType = gi.getGType
25
+ export const System = gi.System
package/lib/inspect.js CHANGED
@@ -3,7 +3,7 @@ const chalk = require('chalk')
3
3
 
4
4
  const internal = require('./native.js')
5
5
  const gi = require('../lib/index');
6
- gi.startLoop();
6
+ require('./loop.js').start();
7
7
 
8
8
  const infos = []
9
9
 
package/lib/loop.js CHANGED
@@ -58,6 +58,11 @@ function start() {
58
58
  * @returns {*} the native return value, or undefined when deferred
59
59
  */
60
60
  function runLoopEntry(run) {
61
+ // The loop integration must be active before we block in a native main loop,
62
+ // so start it automatically on the first run. Calling startLoop() explicitly
63
+ // still works and is no longer required (start() is idempotent).
64
+ start()
65
+
61
66
  if (internal.IsRunningMicrotasks()) {
62
67
  setImmediate(run)
63
68
  return undefined
package/lib/module.js CHANGED
@@ -129,6 +129,12 @@ function listAvailableModules() {
129
129
  // Helpers
130
130
 
131
131
  function parseModuleFilename(filename) {
132
- const [name, version] = filename.replace('.typelib', '').split('-')
133
- return { name, version }
132
+ // Typelibs are named `Name-Version.typelib`; the version is the trailing
133
+ // `-X.Y`, so split on the LAST dash — namespaces themselves may contain dashes
134
+ // (e.g. `GUPnP-DLNA-2.0` -> name `GUPnP-DLNA`, version `2.0`).
135
+ const base = filename.replace(/\.typelib$/, '')
136
+ const dash = base.lastIndexOf('-')
137
+ if (dash === -1)
138
+ return { name: base, version: '' }
139
+ return { name: base.slice(0, dash), version: base.slice(dash + 1) }
134
140
  }
package/lib/native.js CHANGED
@@ -4,10 +4,61 @@
4
4
 
5
5
  const binary = require('@mapbox/node-pre-gyp')
6
6
  const path = require('path')
7
+ const fs = require('fs')
7
8
 
8
9
  const packagePath = path.resolve(path.join(__dirname,'../package.json'))
9
10
  const bindingPath = binary.find(packagePath)
10
11
 
12
+ // On Windows, the prebuilt binary ships with its whole GTK runtime bundled in
13
+ // the same directory as the .node (DLLs, GObject-Introspection typelibs, and
14
+ // runtime data). Wire the process environment to that bundle BEFORE the addon
15
+ // is loaded, so a plain `npm install node-gtk` works with no MSYS2, no compiler
16
+ // and no manual PATH setup. Done in this file specifically because it must run
17
+ // before `require(bindingPath)` (the addon's DLL imports are resolved at load
18
+ // time) and before bootstrap.js requires the GIRepository typelib.
19
+ if (process.platform === 'win32')
20
+ setupBundledRuntime(path.dirname(bindingPath))
21
+
22
+ function setupBundledRuntime(bundleDir) {
23
+ // A self-contained prebuilt always bundles its typelibs; if that marker is
24
+ // absent the addon was built from source against a system GTK, so leave the
25
+ // environment alone.
26
+ const typelibDir = path.join(bundleDir, 'girepository-1.0')
27
+ if (!fs.existsSync(typelibDir))
28
+ return
29
+
30
+ const prepend = (name, value) => {
31
+ const cur = process.env[name]
32
+ process.env[name] = cur ? value + path.delimiter + cur : value
33
+ }
34
+ const exists = p => { try { return fs.existsSync(p) } catch (e) { return false } }
35
+
36
+ // 1) DLLs — both the addon's own imports and the namespace shared libraries
37
+ // GObject-Introspection loads at runtime via g_module_open().
38
+ prepend('PATH', bundleDir)
39
+
40
+ // 2) GI typelibs.
41
+ prepend('GI_TYPELIB_PATH', typelibDir)
42
+
43
+ // 3) Runtime data: icon themes, GtkSourceView languages/styles, schemas.
44
+ const share = path.join(bundleDir, 'share')
45
+ if (exists(share))
46
+ prepend('XDG_DATA_DIRS', share)
47
+ const schemas = path.join(share, 'glib-2.0', 'schemas')
48
+ if (exists(schemas) && !process.env.GSETTINGS_SCHEMA_DIR)
49
+ process.env.GSETTINGS_SCHEMA_DIR = schemas
50
+
51
+ // 4) gdk-pixbuf image loaders (made path-portable at bundle time). The
52
+ // loaders dir goes on PATH so g_module_open() of a bare loader name from
53
+ // the cache resolves.
54
+ const loadersDir = path.join(bundleDir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders')
55
+ if (exists(loadersDir))
56
+ prepend('PATH', loadersDir)
57
+ const loaderCache = path.join(bundleDir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders.cache')
58
+ if (exists(loaderCache) && !process.env.GDK_PIXBUF_MODULE_FILE)
59
+ process.env.GDK_PIXBUF_MODULE_FILE = loaderCache
60
+ }
61
+
11
62
  const binding = require(bindingPath)
12
63
 
13
64
  module.exports = binding
@@ -11,17 +11,50 @@ const GObject = module_.require('GObject')
11
11
 
12
12
  module.exports = registerClass
13
13
 
14
+ // Make registerClass() optional: the first `new Subclass()` of an unregistered
15
+ // JS subclass lazily registers it through this hook (see GObjectConstructor in
16
+ // src/gobject.cc). registerClass() stays available for callers that need the
17
+ // GType before constructing (e.g. getGType, GtkBuilder templates).
18
+ internal.SetLazyClassRegister(registerClass)
19
+
20
+ // A registered class owns its `__gtype__` (native classes via the prototype
21
+ // template, JS classes via registerClass below); an unregistered subclass only
22
+ // inherits one. Used to make registration idempotent and to find ancestors that
23
+ // still need registering.
24
+ function isRegistered(klass) {
25
+ return Object.prototype.hasOwnProperty.call(klass.prototype, '__gtype__')
26
+ }
27
+
14
28
  /**
15
- * Create a new GObject type
29
+ * Create a new GObject type.
30
+ *
31
+ * To override a virtual function, define a method named `virtual_` + the
32
+ * camelCase vfunc name (e.g. `virtual_sizeAllocate` overrides
33
+ * `size_allocate`, `virtual_getRequestMode` overrides `get_request_mode`).
34
+ * Only `virtual_*`-prefixed methods are wired into the vtable; plain methods are
35
+ * never treated as overrides. Chain up to the parent implementation with
36
+ * `super.virtual_sizeAllocate(...)`. See doc/index.md "Inheritance".
37
+ *
16
38
  * @param {Class} klass - The class to register
17
39
  * @param {string} [klass.GTypeName] - The name of the GType (klass.name by default)
40
+ * @returns {Class} the same class (so it can be assigned or used as a decorator)
18
41
  */
19
42
  function registerClass(klass) {
43
+ // Idempotent: a class that already owns a GType is registered. This also makes
44
+ // the lazy-on-first-construct path a no-op for explicitly-registered classes.
45
+ if (isRegistered(klass))
46
+ return klass
47
+
20
48
  const parent = Object.getPrototypeOf(klass.prototype).constructor
21
49
 
22
50
  if (!(klass.prototype instanceof GObject.Object))
23
51
  throw new Error(`Invalid base class (${parent.name})`)
24
52
 
53
+ // Register any unregistered ancestor first, so a subclass can be constructed
54
+ // (or registered) without its superclass having been registered by hand.
55
+ if (!isRegistered(parent))
56
+ registerClass(parent)
57
+
25
58
  const name = createGTypeName(klass)
26
59
  const gtype = GObject.typeFromName(name)
27
60
  const parentName = getGTypeName(parent)
@@ -41,26 +74,51 @@ function registerClass(klass) {
41
74
 
42
75
  // Setup virtual functions
43
76
  setupVirtualFunctions(klass, klassGtype, parentGtype)
77
+
78
+ return klass
44
79
  }
45
80
 
46
81
  // Helpers
47
82
 
83
+ /* Methods whose name starts with `virtual_` are treated as virtual-function
84
+ * overrides — and *only* those. This makes overriding explicit and opt-in: a
85
+ * plain method named `dispose`, `getProperty`, `sizeAllocate`, … can no longer
86
+ * silently hijack the matching GObject vfunc (issue #457). The prefix also keeps
87
+ * the override name distinct from the public invoker method of the same vfunc
88
+ * (e.g. `widget.sizeAllocate(...)` the method vs. the `virtual_sizeAllocate`
89
+ * override), so the two no longer collide. */
90
+ const VIRTUAL_PREFIX = /^virtual_/
91
+
92
+ /* `virtual_getRequestMode` -> `get_request_mode` (drop the prefix, snake_case the
93
+ * rest). snakeCase('virtual_getRequestMode') === 'virtual_get_request_mode'. */
94
+ function vfuncNativeName(key) {
95
+ return snakeCase(key).replace(/^virtual_/, '')
96
+ }
97
+
48
98
  function setupVirtualFunctions(klass, klassGtype, parentGtype) {
49
99
  const parentInfo = findInfoByGtype(parentGtype)
50
100
  if (!parentInfo)
51
101
  throw new Error(`Could not find GIR data in inheritance chain`)
52
102
 
103
+ const parentPrototype = Object.getPrototypeOf(klass.prototype)
104
+
53
105
  Object.getOwnPropertyNames(klass.prototype).forEach(key => {
54
106
  if (key === 'constructor')
55
107
  return
108
+ if (!VIRTUAL_PREFIX.test(key))
109
+ return
56
110
  if (typeof klass.prototype[key] !== 'function')
57
111
  return
58
112
 
59
- const nativeName = snakeCase(key)
113
+ const nativeName = vfuncNativeName(key)
60
114
  const vfuncInfo = findVFunc(klassGtype, parentInfo, nativeName)
61
115
 
62
116
  if (!vfuncInfo)
63
- return
117
+ throw new Error(
118
+ `${klass.name}.${key}: no virtual function '${nativeName}' on ` +
119
+ `'${GObject.typeName(parentGtype)}' or its interfaces. A 'virtual_*' ` +
120
+ `method must name an existing vfunc (e.g. 'virtual_sizeAllocate' for ` +
121
+ `'size_allocate'); rename it if it is a plain method.`)
64
122
 
65
123
  internal.RegisterVFunc(
66
124
  vfuncInfo,
@@ -68,6 +126,31 @@ function setupVirtualFunctions(klass, klassGtype, parentGtype) {
68
126
  nativeName,
69
127
  klass.prototype[key]
70
128
  )
129
+
130
+ installParentVFunc(parentPrototype, parentGtype, key, vfuncInfo)
131
+ })
132
+ }
133
+
134
+ /* Make `super.<vfunc>(...)` reachable from an override. The override replaces
135
+ * the parent's implementation in the class vtable, so a JS subclass otherwise
136
+ * has no way to call the implementation it overrode. We install, on the parent
137
+ * GI class's prototype, a method that invokes the *parent's* native vfunc impl
138
+ * (resolved through `parentGtype`'s vtable, not the overriding subclass's).
139
+ *
140
+ * Only the native boundary needs bridging: if the parent prototype already owns
141
+ * `key` — i.e. the parent is itself a registered JS class that overrode this
142
+ * vfunc — then `super.<vfunc>()` resolves to that JS method on its own. */
143
+ function installParentVFunc(parentPrototype, parentGtype, key, vfuncInfo) {
144
+ if (Object.prototype.hasOwnProperty.call(parentPrototype, key))
145
+ return
146
+
147
+ Object.defineProperty(parentPrototype, key, {
148
+ value: function (...args) {
149
+ return internal.CallVFunc(vfuncInfo, parentGtype, this, args)
150
+ },
151
+ writable: true,
152
+ configurable: true,
153
+ enumerable: false,
71
154
  })
72
155
  }
73
156