swing-ui 0.1.25 → 1.0.3

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.
Files changed (116) hide show
  1. package/LICENSE +5 -4
  2. package/README.md +144 -7
  3. package/SwingUI.js +2611 -0
  4. package/classes/Arc.js +185 -0
  5. package/classes/Button.js +55 -0
  6. package/classes/CheckBox.js +33 -0
  7. package/classes/CheckBoxMenuItem.js +24 -0
  8. package/classes/ComboBox.js +329 -0
  9. package/classes/ComponentContainer.js +828 -0
  10. package/classes/ContextMenu.js +141 -0
  11. package/classes/DocumentEditor.js +168 -0
  12. package/classes/Ellipse.js +76 -0
  13. package/classes/InternalWindow.js +397 -0
  14. package/classes/InternalWindowContainer.js +93 -0
  15. package/classes/Label.js +52 -0
  16. package/classes/Line.js +105 -0
  17. package/classes/List.js +452 -0
  18. package/classes/MediaPlayer.js +504 -0
  19. package/classes/Menu.js +74 -0
  20. package/classes/MenuBar.js +56 -0
  21. package/classes/MenuItem.js +92 -0
  22. package/classes/Panel.js +50 -0
  23. package/classes/PasswordField.js +47 -0
  24. package/classes/Polygon.js +236 -0
  25. package/classes/Polyline.js +240 -0
  26. package/classes/ProgressBar.js +62 -0
  27. package/classes/RadioButton.js +110 -0
  28. package/classes/RadioButtonMenuItem.js +106 -0
  29. package/classes/Rectangle.js +199 -0
  30. package/classes/ScrollPane.js +53 -0
  31. package/classes/ScrollableComponent.js +141 -0
  32. package/classes/Separator.js +24 -0
  33. package/classes/Shape.js +584 -0
  34. package/classes/ShapeContainer.js +231 -0
  35. package/classes/Slider.js +172 -0
  36. package/classes/Spinner.js +172 -0
  37. package/classes/SplitView.js +82 -0
  38. package/classes/Stroke.js +115 -0
  39. package/classes/Table.js +982 -0
  40. package/classes/Tabs.js +228 -0
  41. package/classes/TextArea.js +125 -0
  42. package/classes/TextComponent.js +207 -0
  43. package/classes/TextField.js +128 -0
  44. package/classes/TextShape.js +123 -0
  45. package/classes/ToggleButton.js +147 -0
  46. package/classes/ToolBar.js +120 -0
  47. package/classes/TrayIcon.js +54 -0
  48. package/classes/Tree.js +569 -0
  49. package/classes/UIComponent.js +2154 -0
  50. package/classes/Window.js +609 -0
  51. package/enums.js +298 -0
  52. package/fonts.conf +32 -0
  53. package/functions.js +254 -0
  54. package/icon.png +0 -0
  55. package/index.js +109 -4
  56. package/jar/gui-11.jar +0 -0
  57. package/jar/lib/flatlaf.jar +0 -0
  58. package/jar/lib/jna.jar +0 -0
  59. package/package.json +58 -33
  60. package/typedefs.js +973 -0
  61. package/types/SwingUI.d.ts +727 -0
  62. package/types/classes/Arc.d.ts +111 -0
  63. package/types/classes/Button.d.ts +38 -0
  64. package/types/classes/CheckBox.d.ts +22 -0
  65. package/types/classes/CheckBoxMenuItem.d.ts +18 -0
  66. package/types/classes/ComboBox.d.ts +172 -0
  67. package/types/classes/ComponentContainer.d.ts +131 -0
  68. package/types/classes/ContextMenu.d.ts +74 -0
  69. package/types/classes/DocumentEditor.d.ts +95 -0
  70. package/types/classes/Ellipse.d.ts +34 -0
  71. package/types/classes/InternalWindow.d.ts +163 -0
  72. package/types/classes/InternalWindowContainer.d.ts +36 -0
  73. package/types/classes/Label.d.ts +33 -0
  74. package/types/classes/Line.d.ts +48 -0
  75. package/types/classes/List.d.ts +223 -0
  76. package/types/classes/MediaPlayer.d.ts +381 -0
  77. package/types/classes/Menu.d.ts +43 -0
  78. package/types/classes/MenuBar.d.ts +26 -0
  79. package/types/classes/MenuItem.d.ts +57 -0
  80. package/types/classes/Panel.d.ts +28 -0
  81. package/types/classes/PasswordField.d.ts +22 -0
  82. package/types/classes/Polygon.d.ts +93 -0
  83. package/types/classes/Polyline.d.ts +96 -0
  84. package/types/classes/ProgressBar.d.ts +36 -0
  85. package/types/classes/RadioButton.d.ts +70 -0
  86. package/types/classes/RadioButtonMenuItem.d.ts +71 -0
  87. package/types/classes/Rectangle.d.ts +82 -0
  88. package/types/classes/ScrollPane.d.ts +23 -0
  89. package/types/classes/ScrollableComponent.d.ts +81 -0
  90. package/types/classes/Separator.d.ts +14 -0
  91. package/types/classes/Shape.d.ts +223 -0
  92. package/types/classes/ShapeContainer.d.ts +92 -0
  93. package/types/classes/Slider.d.ts +107 -0
  94. package/types/classes/Spinner.d.ts +129 -0
  95. package/types/classes/SplitView.d.ts +47 -0
  96. package/types/classes/Stroke.d.ts +99 -0
  97. package/types/classes/Table.d.ts +652 -0
  98. package/types/classes/Tabs.d.ts +118 -0
  99. package/types/classes/TextArea.d.ts +45 -0
  100. package/types/classes/TextComponent.d.ts +136 -0
  101. package/types/classes/TextField.d.ts +94 -0
  102. package/types/classes/TextShape.d.ts +73 -0
  103. package/types/classes/ToggleButton.d.ts +84 -0
  104. package/types/classes/ToolBar.d.ts +82 -0
  105. package/types/classes/TrayIcon.d.ts +43 -0
  106. package/types/classes/Tree.d.ts +354 -0
  107. package/types/classes/UIComponent.d.ts +966 -0
  108. package/types/classes/Window.d.ts +334 -0
  109. package/types/enums.d.ts +437 -0
  110. package/types/functions.d.ts +109 -0
  111. package/types/index.d.ts +188 -0
  112. package/types/typedefs.d.ts +1626 -0
  113. package/ui-process-manager.js +317 -0
  114. package/video-devices-linux.sh +72 -0
  115. package/video-devices-mac.sh +33 -0
  116. package/CHANGES.md +0 -3
package/SwingUI.js ADDED
@@ -0,0 +1,2611 @@
1
+ import {Worker, MessageChannel, receiveMessageOnPort} from "worker_threads";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import zlib from "zlib"; // https://nodejs.org/api/zlib.html#class-options
5
+ import {execFileSync} from "node:child_process";
6
+
7
+ /**
8
+ * Main SwingUI class that handles all communication between JavaScript and GUI and provides many utility methods not related to specific GUI components.
9
+ * <b>NOTE:</b> Do not instantiate this class, but instead use global `ui` constant, which is an instance of this class.
10
+ *
11
+ * See usage example below.
12
+ *
13
+ * @class
14
+ * @global
15
+ * @see ui
16
+ * @example <h2>Usage examples:</h2>
17
+ * const data = ui.clipboard(); // getting clipboard data
18
+ * const _screens = ui.screens; // getting screens information array
19
+ * ui.openURL("http://swing-ui.com/");
20
+ */
21
+ export class SwingUI
22
+ {
23
+ /**
24
+ * @internal
25
+ * @private
26
+ */
27
+ stats = {in: 0, out: 0, start: new Date()};
28
+ #gui_version = "0.3.1"; // (Not a module version. For internal use only.)
29
+ /**
30
+ * @internal
31
+ * @private
32
+ */
33
+ gui_process_id = null;
34
+ #module_dir = undefined;
35
+ #root_dir = undefined;
36
+ #js_runtime = typeof Bun !== "undefined" ? "bun" : typeof Deno !== "undefined" ? "deno" : typeof process !== "undefined" && process.versions && process.versions.node ? "node" : "unknown";
37
+ #watchdog_timer = null;
38
+ #sleepBuffer = new Int32Array(new SharedArrayBuffer(4)); // used in ui.sleep(N)
39
+
40
+ #processingStack = false;
41
+ #_msg_queue = [];
42
+ #queue_limit = 100; // send queued messages after queue size reaches this limit (0 = no message queue)
43
+ #msgCompressThreshold = 1000; // compress messages longer than this
44
+ #textDecoder = new TextDecoder("utf8");
45
+
46
+ #jvm = {
47
+ stdio: {}, JBridge: null, process: null, eventListener: e =>
48
+ {
49
+ }
50
+ };
51
+ #guiManagerWorker = null; // for GUI Process manager worker
52
+ #exiting = false;
53
+
54
+ /**
55
+ * @internal
56
+ * @private
57
+ */
58
+ callback = {
59
+ //Error handler function that ui.onError can override(callback)
60
+ onError: function(e)
61
+ {
62
+ dialog(e.message.replaceAll("<", "&lt;").replaceAll(">", "&gt;"), e.target !== undefined ? `Error with ${e.target.constructor.name}#${e.target.id}` : `GUI Error`, ["OK"], MessageIcon.Error, e.target !== undefined ? e.target.window : null);
63
+ }, // Replace HTML tag arrows to make sure they are not rendered as HTML in GUI dialog window
64
+ onAppEvents: [
65
+ // Event handler to handle Exit message from GUI
66
+ e =>
67
+ {
68
+ if(e.type === "Exit") this.exit(e.code);
69
+ }
70
+ ]
71
+ }; // for global events
72
+
73
+ /**
74
+ * @internal
75
+ * @private
76
+ */
77
+ _msg = {
78
+ 0: "Calling a function of a destroyed UI component",
79
+ 1: "Cannot add UI component to a not UI component",
80
+ 2: "Argument should be UIComponent, but received 'undefined'.",
81
+ 3: "Unidentified message type received from GUI: ",
82
+ 4: "Unhandled Event: ",
83
+ 5: "Invalid response to message received: ",
84
+ 6: "Node.js Exception when processing event on ",
85
+ 7: "Node.js Exception when processing response.",
86
+ 8: "Application root directory could not be detected. Either start the project from the project directory, set it as a working directory, or use the --root-dir=\"DIRECTORY_PATH\" option to specify the project directory. Current working directory: ",
87
+ 9: "Error while decoding message from GUI: ",
88
+ 10: "Failed to set context menu. Passed parameter must be of type ContextMenu and can't be added to self",
89
+ 11: "Failed to set menu menu. Passed parameter must be of type MenuBar()"
90
+ };
91
+
92
+ //region For Worker IPC only
93
+
94
+ #useWorkerThread = false;
95
+ #worker_port1 = null;
96
+ #worker_port2 = null;
97
+
98
+ /**
99
+ * @internal
100
+ * @private
101
+ */
102
+ static WorkerSignalIndex = Object.freeze({
103
+ ExecutionControl: 0,
104
+ WorkerControl: 1,
105
+ DataLength: 2,
106
+ ReceivedBytes: 3,
107
+ SentBytes: 4
108
+ });
109
+
110
+ /**
111
+ * @internal
112
+ * @private
113
+ */
114
+ static WorkerSignal = Object.freeze({Resume: 0, Pause: 1, SendLine: 2, Block: 3, Unblock: 4, SendBytes: 5});
115
+
116
+ #sharedBuffSignal = new Int32Array(new SharedArrayBuffer(4));
117
+
118
+ //endregion
119
+
120
+ #inputLinesBuffer = new class
121
+ {
122
+ _buff = [];
123
+ pos = 0;
124
+ max = 200;
125
+ timer = null;
126
+
127
+ get length()
128
+ {
129
+ return this._buff.length - this.pos;
130
+ }
131
+
132
+ push(val)
133
+ {
134
+ this._buff.push(val);
135
+ }
136
+
137
+ pull()
138
+ {
139
+ if(this.pos < this._buff.length)
140
+ {
141
+ const val = this._buff[this.pos++];
142
+
143
+ if(this.pos > this.max && this.timer === null)
144
+ {
145
+ this.timer = setTimeout(() =>
146
+ {
147
+ this._buff = this._buff.slice(this.pos);
148
+ this.pos = 0;
149
+ this.timer = null;
150
+ }, 1000);
151
+ }
152
+
153
+ return val;
154
+ }
155
+
156
+ return undefined;
157
+ }
158
+ }();
159
+
160
+ /**
161
+ * Detects best compatible base64 decoder
162
+ */
163
+ #decodeBase64 = typeof Uint8Array.fromBase64 === 'function' ? b64 => Uint8Array.fromBase64(b64) : typeof Buffer !== 'undefined' ? b64 => Buffer.from(b64, 'base64') : b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
164
+
165
+ //region Internal
166
+
167
+ /**
168
+ * <b>NOTE:</b> Do not instantiate this class. It's already instantiated and assigned to a global variable `ui`.
169
+ * @internal
170
+ * @private
171
+ */
172
+ constructor(module_dir, JBridge)
173
+ {
174
+ this.#module_dir = module_dir;
175
+ this.#jvm.JBridge = JBridge;
176
+ this.rootDirectory; // detect project dir now
177
+
178
+ // ensure cleanup
179
+ process.on("exit", this.exit.bind(this));
180
+ process.on("SIGINT", this.exit.bind(this, 0, "SIGINT")); //catches ctrl+c event
181
+
182
+ // catches "kill pid" (for example: nodemon restart) on some operating systems
183
+ process.on("SIGUSR1", this.exit.bind(this, 0, "SIGUSR1"));
184
+ process.on("SIGUSR2", this.exit.bind(this, 0, "SIGUSR2"));
185
+ process.on("SIGTERM", this.exit.bind(this, 0, "SIGTERM"));
186
+ process.on("SIGHUP", this.exit.bind(this, 0, "SIGHUP"));
187
+ }
188
+
189
+ async InitGUIProcess()
190
+ {
191
+ if(this.runtime === "bun" || this.os === "darwin") this.#useWorkerThread = true;
192
+
193
+ const env = this.env;
194
+
195
+ if(process.argv.some(arg => arg.startsWith("--java-dir="))) env.JAVA_HOME = process.argv.find(arg => arg.startsWith("--java-dir=")).split("=")[1];
196
+
197
+ if(this.#useWorkerThread)
198
+ {
199
+ this.#msgCompressThreshold = 500; // reduce the threshold
200
+
201
+ const {port1, port2} = new MessageChannel();
202
+ this.#worker_port1 = port1;
203
+ this.#worker_port2 = port2;
204
+
205
+ // start IPC Worker
206
+ const workerPath = path.join(this.#module_dir, "ui-process-manager.js");
207
+
208
+ // set a value for the thread to wait for the worker to start
209
+ Atomics.store(this.#sharedBuffSignal, SwingUI.WorkerSignalIndex.ExecutionControl, SwingUI.WorkerSignal.Block);
210
+
211
+ const options = {
212
+ type: "module",
213
+ workerData: {
214
+ signalBuffer: this.#sharedBuffSignal,
215
+ responsePort: this.#worker_port2,
216
+ moduleDir: this.#module_dir,
217
+ rootDir: this.rootDirectory,
218
+ debugJava: process.argv.some(arg => arg === "--debug-java"),
219
+ verbose: process.argv.some(arg => arg === "--v"),
220
+ os: this.os,
221
+ runtime: this.runtime
222
+ },
223
+ transferList: [this.#worker_port2],
224
+ stdout: true,
225
+ stderr: true,
226
+ env: env
227
+ };
228
+
229
+ this.#guiManagerWorker = new Worker(workerPath, options);
230
+
231
+ // forward worker logs to the main process
232
+ if(this.#guiManagerWorker.stdout != null) // e.g.. bun, and deno on macOS don't have stdout
233
+ {
234
+ this.#guiManagerWorker.stdout.on("data", chunk => console.log(chunk.toString()));
235
+ this.#guiManagerWorker.stderr.on("data", chunk => console.error(chunk.toString()));
236
+ }
237
+
238
+ this.#guiManagerWorker.on("message", (msg) =>
239
+ {
240
+ switch(msg.type)
241
+ {
242
+ case "data":
243
+ {
244
+ this.#inputLinesBuffer.push(msg.data);
245
+ if(this.#inputLinesBuffer.length < 2) queueMicrotask(() => {this.#processInputData();});
246
+ break;
247
+ }
248
+ case "exit":
249
+ {
250
+ this.exit(0);
251
+ break;
252
+ }
253
+ case "worker_log":
254
+ {
255
+ console.log(`WORKER THREAD: ${msg.data}`);
256
+ break;
257
+ }
258
+ case "worker_debug":
259
+ {
260
+ console.debug(`WORKER THREAD: ${msg.data}`);
261
+ break;
262
+ }
263
+ case "worker_warn":
264
+ {
265
+ console.warn(`WORKER THREAD: ${msg.data}`);
266
+ break;
267
+ }
268
+ case "worker_info":
269
+ {
270
+ console.info(`WORKER THREAD: ${msg.data}`);
271
+ break;
272
+ }
273
+ case "worker_error":
274
+ {
275
+ console.error(`WORKER THREAD ERROR: ${msg.data}`);
276
+ break;
277
+ }
278
+ }
279
+ });
280
+ this.#guiManagerWorker.on("online", () => {});
281
+ this.#guiManagerWorker.on("error", (err) => console.error("[SwingUI] GUI Worker error:", err));
282
+ this.#guiManagerWorker.on("exit", (code) =>
283
+ {
284
+ if(!this.#exiting && code !== 0) console.error(`[SwingUI] GUI Worker stopped unexpectedly (exit code ${code})`);
285
+ this.exit(code);
286
+ });
287
+
288
+ // wait for the worker to start
289
+ const r = Atomics.wait(this.#sharedBuffSignal, SwingUI.WorkerSignalIndex.ExecutionControl, SwingUI.WorkerSignal.Block, 10000);
290
+ if(r === "timed-out") throw new Error("Failed to start GUI Worker: timeout.");
291
+ }
292
+ else
293
+ {
294
+ const jvmOpts = {opts: [], env: env};
295
+ if(process.argv.some(arg => arg === "--debug-java")) jvmOpts.opts.push("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005");
296
+ if(this.os === "linux") jvmOpts.opts.push("--add-opens=java.desktop/sun.awt.X11=ALL-UNNAMED"); // to be able to set WM_CLASS in Java GUI
297
+ if(this.os === "darwin")
298
+ {
299
+ jvmOpts.opts.push("-Dapple.awt.UIElement=true");
300
+ jvmOpts.opts.push("-Dapple.awt.headless=false"); // was supposed to help JVM on macOS, but didn't. macOS doesn't allow GUI on non-main thread.
301
+ }
302
+
303
+ this.#jvm.JBridge.ensureJvm(jvmOpts);
304
+ this.#jvm.JBridge.appendClasspath(path.join(this.#module_dir, "jar/gui-11.jar"));
305
+ const JBridgeProcessHandle = this.#jvm.JBridge.importClass("java.lang.ProcessHandle");
306
+ this.#jvm.process = await JBridgeProcessHandle.current();
307
+ this.gui_process_id = Number(await this.#jvm.process.pid());
308
+
309
+ // receive events from GUI
310
+ this.#jvm.stdio.stdout = (_, line) =>
311
+ {
312
+ line = line.trim();
313
+ if(!line) return;
314
+
315
+ if(line.startsWith("GUI-EVENT->"))
316
+ {
317
+ try
318
+ {
319
+ const message = this.#textDecoder.decode(this.#decodeBase64(line.substring(11)));
320
+ this.#inputLinesBuffer.push(message);
321
+ }catch(ex)
322
+ {
323
+ console.error("Failed to parse IPC event:", ex);
324
+ }
325
+ }
326
+ else
327
+ {
328
+ console.log(line);
329
+ }
330
+
331
+ this.#processInputData();
332
+ };
333
+
334
+ this.#jvm.stdio.stderr = (_, data) =>
335
+ {
336
+ console.error(data);
337
+ };
338
+
339
+ // enable STDIO redirect to our handlers
340
+ this.#jvm.stdio.guard = this.#jvm.JBridge.stdout.enableRedirect(this.#jvm.stdio.stdout, this.#jvm.stdio.stderr);
341
+
342
+ /*
343
+ this.#jvm.eventListener = this.#jvm.JBridge.newProxy("SwingUI$Callback", {
344
+ eventCall: data =>
345
+ {
346
+ if(this.#inputLinesBuffer.length === 0) queueMicrotask(() => {this.#processInputData();});
347
+
348
+ data = this.#textDecoder.decode(this.#decodeBase64(data));
349
+
350
+ this.#inputLinesBuffer.push(data);
351
+ }
352
+ });
353
+ */
354
+
355
+ const ui_class = this.#jvm.JBridge.importClass("SwingUI");
356
+ const ops = JSON.stringify({
357
+ module_dir: this.#module_dir,
358
+ root_dir: this.rootDirectory,
359
+ verbose: process.argv.some(arg => arg === "--v")
360
+ });
361
+ this.#jvm.gui = new ui_class(ops);
362
+ //await this.#jvm.gui.setEventCallback(this.#jvm.eventListener);
363
+
364
+ // Watchdog
365
+ this.#watchdog_timer = setInterval(() =>
366
+ {
367
+ try
368
+ {
369
+ process.kill(this.gui_process_id, 0);
370
+ }catch(ex)
371
+ {
372
+ console.log("Watchdog timer says the GUI process exited, so JS can exit too.");
373
+ if(ex.code === "ESRCH") this.exit(1);
374
+ }
375
+ }, 5000);
376
+ }
377
+
378
+ // report current version of this module to UI
379
+ this.sendToGUI({system: "version", value: this.#gui_version, pid: this.pid}, async r =>
380
+ {
381
+ if(!r.confirmed)
382
+ {
383
+ this.sendToGUI = m =>
384
+ {
385
+ }; // stop sending messages
386
+ setTimeout(() =>
387
+ {
388
+ this.exit(0);
389
+ }, 10000); // exit automatically if not closed by parent process
390
+ console.error("GUI version verification failed!");
391
+ }
392
+ }, true);
393
+ }
394
+
395
+ #processInputData()
396
+ {
397
+ this.#processingStack++;
398
+
399
+ // process one line
400
+ try
401
+ {
402
+ if(this.#inputLinesBuffer.length > 0) this.#processInputLine(this.#inputLinesBuffer.pull());
403
+ }finally
404
+ {
405
+ this.#processingStack--;
406
+
407
+ // keep processing if there is more data
408
+ if(this.#inputLinesBuffer.length > 0)
409
+ {
410
+ queueMicrotask(() => {this.#processInputData();});
411
+ }
412
+ }
413
+ }
414
+
415
+ #processInputLine(msg, callback = null)
416
+ {
417
+ if(typeof msg !== "string" || msg.length === 0) return null;
418
+
419
+ /*try
420
+ {
421
+ msg = decodeURIComponent(msg).trim();
422
+ }catch(ex)
423
+ {
424
+ console.error(this._msg[9] + msg + "\n\n" + ex.stack);
425
+ console.error(ex);
426
+ return null;
427
+ }*/
428
+
429
+ if(msg.startsWith("{") && msg.endsWith("}"))
430
+ {
431
+ try
432
+ {
433
+ msg = JSON.parse(msg);
434
+ }catch(ex)
435
+ {
436
+ error(ex + "\n While parsing JSON message from GUI in ui.processInputLine(" + msg + ")\r\n" + ex.stack.toString(), false);
437
+ return null;
438
+ }
439
+
440
+ // run response callback if passed
441
+ if(callback !== null)
442
+ {
443
+ try
444
+ {
445
+ return callback(msg) ?? null;
446
+ }catch(ex)
447
+ {
448
+ error(ex + this._msg[7], false);
449
+ return null;
450
+ }
451
+ }
452
+
453
+ // process message
454
+ return this.#processJsonMessage(msg);
455
+ }
456
+ else // treat as CLI input
457
+ {
458
+ if(Array.isArray(this.callback.onCLI))
459
+ {
460
+ for(let f of this.callback.onCLI)
461
+ {
462
+ try
463
+ {
464
+ f({data: msg});
465
+ }catch(ex)
466
+ {
467
+ error(`${ex.name} occurred in one of onCLI(callback) event handlers:\n\n${ex.stack}`, false);
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ return false;
474
+ }
475
+
476
+ /**
477
+ * @param {Object} msg
478
+ * @return {any}
479
+ */
480
+ #processJsonMessage(msg)
481
+ {
482
+ // check for events
483
+ if(msg.event !== undefined)
484
+ {
485
+ try
486
+ {
487
+ return this.#processEvent(msg);
488
+ }catch(ex)
489
+ {
490
+ error(`Your ${msg.event}(event) handler has an error:\n\n${ex.stack} \n\nEvent ${stringify(ex)}`, false);
491
+ }
492
+ }
493
+ else if("heartbeat" in msg)
494
+ {
495
+ // do nothing
496
+ }
497
+ else
498
+ {
499
+ alert(this._msg[3] + JSON.stringify(msg));
500
+ return false;
501
+ }
502
+ }
503
+
504
+ #processEvent(e)
505
+ {
506
+ try
507
+ {
508
+ if(e.id === undefined) // check if event has a component ID
509
+ {
510
+ return (e.event === "onError") ? this.callback.onError(e) : null; // handle general errors not related to a specific component
511
+ }
512
+ else if(e.id === 0) // for global events
513
+ {
514
+ e.target = this;
515
+ delete e.id;
516
+ }
517
+ else
518
+ {
519
+ e.target = UIComponent.get(e.id); // get target control
520
+ if(e.target === undefined || e.target.destroyed) return; // don't process events for destroyed components
521
+ delete e.id;
522
+ }
523
+
524
+ // checks if target was already destroyed
525
+ if(e.target == null || e.target.callback == null) return;
526
+
527
+ e.time = new Date();
528
+
529
+ // Update some value in event
530
+ switch(e.event)
531
+ {
532
+ case "onPropertyChange":
533
+ {
534
+ if(e.property === "ancestor")
535
+ {
536
+ // convert component IDs to Objects
537
+ if(e.oldValue !== undefined) e.oldValue = UIComponent.get(e.oldValue);
538
+ if(e.newValue !== undefined) e.newValue = UIComponent.get(e.newValue);
539
+ e.handled = true;
540
+ }
541
+ break;
542
+ }
543
+ case "onComponentEvents":
544
+ {
545
+ // update size and location of component locally
546
+ if(e.type === "resize")
547
+ {
548
+ e.target.emit("onRectangleUpdate", {
549
+ target: e.target,
550
+ type: e.type,
551
+ width: e.width,
552
+ height: e.height
553
+ });
554
+ e.handled = true;
555
+ }
556
+ else if(e.type === "move")
557
+ {
558
+ e.target.emit("onRectangleUpdate", {
559
+ target: e.target,
560
+ type: e.type,
561
+ x: e.x,
562
+ y: e.y
563
+ });
564
+ e.handled = true;
565
+ if("invoker" in e) e.target.prop.invoker = e.invoker == null ? null : UIComponent.get(e.invoker); // for context menu only
566
+ }
567
+ break;
568
+ }
569
+ case "onAncestorEvents":
570
+ {
571
+ if(e.ancestor !== undefined) e.ancestor = UIComponent.get(e.ancestor);
572
+ if(e.ancestorParent !== undefined) e.ancestorParent = UIComponent.get(e.ancestorParent);
573
+
574
+ if(e.type.startsWith("removed") && e.skipCallback) // this component was removed from it's parent in GUI, such as JInternalFrame when it's closed, and needs to be removed from parent here
575
+ {
576
+ return e.target.remove();
577
+ }
578
+ e.handled = true;
579
+ break;
580
+ }
581
+ case "onWindowEvents":
582
+ {
583
+ if(e.type === "close" && e.destroy) // if Window is closed, destroy it, because it's no longer reusable
584
+ {
585
+ queueMicrotask(() =>
586
+ {
587
+ e.target.destroy();
588
+ });
589
+ if(e.skipCallback) e.handled = true; // we don't continue, because there will be another event with type "closed" emitted.
590
+ }
591
+ break;
592
+ }
593
+ case "onPopupEvents":
594
+ {
595
+ if(e.target instanceof ContextMenu) // for ContextMenu to get invoker component
596
+ {
597
+ if("invoker" in e) // set new invoker
598
+ {
599
+ e.target.prop.invoker = e.invoker == null ? null : UIComponent.get(e.invoker);
600
+ delete e.invoker;
601
+ }
602
+ if(e.x != null && e.y != null) // set new location of context menu
603
+ {
604
+ e.target.prop.location = {x: e.x, y: e.y};
605
+ delete e.x;
606
+ delete e.y;
607
+ }
608
+ }
609
+ break;
610
+ }
611
+ case "onItemsDropEvents":
612
+ case "onDropEvents":
613
+ {
614
+ if("overComponent" in e) e.overComponent = UIComponent.get(e.overComponent); // convert id to component
615
+
616
+ // update data types of dropped data
617
+ if(typeof e.data === "object")
618
+ {
619
+ // find first valid event source component
620
+ for(let key of Object.keys(e.data))
621
+ {
622
+ try
623
+ {
624
+ if(key.startsWith("component/"))
625
+ {
626
+ let source = UIComponent.get(e.data[key]);
627
+
628
+ // verify the type of source component and if drag is enabled
629
+ if(source && key.endsWith("component/" + source.constructor.name.toLowerCase()) && source.prop.dragAction !== DragDropAction.None)
630
+ {
631
+ e.source = source;
632
+ }
633
+
634
+ delete e.data[key]; // remove component ID
635
+ }
636
+ else if(key === "URIList" && typeof e.data[key] === "string") // for compatibility
637
+ {
638
+ e.data["URIList"] = e.data["URIList"].split("\n");
639
+ }
640
+ else if(key === "Files" && typeof e.data[key] === "string") // for compatibility
641
+ {
642
+ e.data.Files = e.data.Files.split("\n");
643
+ }
644
+ else if(key === "JSON" && typeof e.data[key] === "string") // for compatibility
645
+ {
646
+ e.data["JSON"] = JSON.parse(e.data["JSON"]); // parse object's json data
647
+ }
648
+ }catch(ex)
649
+ {
650
+ }
651
+ }
652
+ }
653
+ break;
654
+ }
655
+ case "onShapeEvents":
656
+ {
657
+ e.shape = e.target.shape?.(e.shapeIndex);
658
+ break;
659
+ }
660
+ }
661
+
662
+ if(Array.isArray(e.target.callback[e.event])) // Catch-All event handler
663
+ {
664
+ for(const callback of e.target.callback[e.event])
665
+ {
666
+ try
667
+ {
668
+ callback.call(e.target, e);
669
+ }catch(ex)
670
+ {
671
+ let comp_name = e.target.constructor.name;
672
+ e.target = `${comp_name}{...}`; // make it shorter by hiding internals of component
673
+ if(e.source != null) e.source = `${e.source.constructor.name}{...}`; // make it shorter by hiding internals of component
674
+ error(`${this._msg[6]}${comp_name}: ${e.event} \n\n${ex.stack}\n\nEvent ${stringify(e)}`, false);
675
+ }
676
+ }
677
+ }
678
+ else if(typeof e.target.callback[e.event] === "function") // if callback is a function, not array of functions
679
+ {
680
+ // call the handler
681
+ e.target.callback[e.event].call(e.target, e);
682
+ }
683
+ else if(e.type !== undefined && typeof e.target.callback[e.type] === "function")
684
+ {
685
+ e.target.callback[e.type].call(e.target, e);
686
+ }
687
+ else if(!e.handled) // if event was not handled
688
+ {
689
+ //if(e.target !== undefined) e.target = (e.target instanceof UIComponent) ? "object of type " + e.target.prop.type : typeof e.target;
690
+ //alert(this._msg[4] + e.event + "\n\n" + stringify(e));
691
+ }
692
+ }catch(ex)
693
+ {
694
+ error(`${this._msg[6]}${e.id}: ${e.event} \n\n${ex.stack}\n\nEvent ${stringify(e)}`, false);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Send a messages to GUI process via STD out. Use internally for communication only.
700
+ * @param {Object} msg - JSON Object message to send to GUI.
701
+ * @param {function(any):any} [callback] - Only pass callback to block event loop and wait for response
702
+ * @return {any|void} Returns response from GUI or undefined.
703
+ * @internal
704
+ * @ignore
705
+ */
706
+ sendToGUI(msg, callback = null)
707
+ {
708
+ // create default callback for "get" messages
709
+ if(callback === null && msg.get !== undefined)
710
+ {
711
+ callback = r =>
712
+ {
713
+ return r.value;
714
+ };
715
+ }
716
+
717
+ // If no callback is needed, then we can stack up messages and send them together
718
+ if(callback != null)
719
+ {
720
+ // Send accumulated messages first
721
+ this.sendMessageQueue();
722
+
723
+ let msgs = JSON.stringify([msg]);
724
+ this.stats.out += msgs.length;
725
+
726
+ let response;
727
+
728
+ if(this.#useWorkerThread)
729
+ {
730
+ response = this.#sendMessagesViaWorker("b64:" + Buffer.from(msgs, "utf-8").toString("base64"), true);
731
+ }
732
+ else
733
+ {
734
+ response = this.#jvm.gui.receiveDirectMessagesSync("b64:" + Buffer.from(msgs, "utf-8").toString("base64"));
735
+ if(response != null) response = this.#textDecoder.decode(this.#decodeBase64(response));
736
+ }
737
+
738
+ if(response != null && typeof response === "string") response = JSON.parse(response);
739
+
740
+ if(response != null && "get" in msg)
741
+ {
742
+ if(msg.get === response.response && msg.control === response.control) return callback(response);
743
+ throw new Error("Response doesn't match request message.");
744
+ }
745
+
746
+ return callback(response);
747
+ }
748
+ else
749
+ {
750
+ this.#_msg_queue.push(structuredClone(msg)); // clone original object to preserve values in case they change before we send the message to GUI
751
+
752
+ if(this.#_msg_queue.length >= this.#queue_limit)
753
+ {
754
+ this.sendMessageQueue();
755
+ }
756
+ else if(this.#_msg_queue.length === 1) // schedule to send messages when event loop gets free
757
+ {
758
+ queueMicrotask(() =>
759
+ {
760
+ this.sendMessageQueue();
761
+ });
762
+ }
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Immediately sends all queued messages to GUI without waiting for queue threshold or execution end. It's similar to flush() function in data streams.
768
+ * It can be used to force messages to be sent imedietly instead of waiting for message queue to be full, if it's important to update some UI component or content right away.
769
+ * It may improve performance to set `compress` to `false` to skip compression for already compressed data, such as UI component's background image or large icon.
770
+ *
771
+ * @param {boolean} [compress=true] - Whether to compress messages before sending.
772
+ * @return {this} Returns `this` instance for chaining.
773
+ * @example <h2>Sending queued messages before proceeding:</h2>
774
+ * ui.sendMessageQueue();
775
+ *
776
+ * @example <h2>Sending queued messages without compressing:</h2>
777
+ * ui.sendMessageQueue(false);
778
+ */
779
+ sendMessageQueue(compress = true)
780
+ {
781
+ if(this.#_msg_queue.length < 1) return this;
782
+
783
+ let msgs = JSON.stringify(this.#_msg_queue);
784
+
785
+ // compress messages if more than one message and long enough
786
+ if(compress && this.#_msg_queue.length > 1 && msgs.length > this.#msgCompressThreshold)
787
+ {
788
+ msgs = "gzip64:" + zlib.gzipSync(msgs, {
789
+ level: zlib.constants.Z_BEST_COMPRESSION,
790
+ strategy: zlib.constants.Z_DEFAULT_STRATEGY
791
+ }).toString("base64");
792
+ }
793
+ else // convert to base64 to make sure non-ASCII characters are transmitted correctly
794
+ {
795
+ msgs = "b64:" + Buffer.from(msgs, "utf-8").toString("base64");
796
+ }
797
+
798
+ this.stats.out += msgs.length;
799
+ this.#_msg_queue = []; // clear queue
800
+
801
+ if(this.#guiManagerWorker != null)
802
+ {
803
+ this.#sendMessagesViaWorker(msgs, false);
804
+ }
805
+ else if(this.#exiting) // when exiting, don't wait for response
806
+ {
807
+ this.#jvm.gui.receiveDirectMessages(msgs);
808
+ }
809
+ else
810
+ {
811
+ this.#jvm.gui.receiveDirectMessagesSync(msgs);
812
+ }
813
+
814
+ return this;
815
+ }
816
+
817
+ #sendMessagesViaWorker(data, get_response = false)
818
+ {
819
+ if(get_response) Atomics.store(this.#sharedBuffSignal, SwingUI.WorkerSignalIndex.ExecutionControl, SwingUI.WorkerSignal.Block);
820
+
821
+ this.#guiManagerWorker.postMessage({data: data, respond: get_response});
822
+
823
+ if(get_response)
824
+ {
825
+ // Block until worker signals
826
+ Atomics.wait(this.#sharedBuffSignal, SwingUI.WorkerSignalIndex.ExecutionControl, SwingUI.WorkerSignal.Block);
827
+
828
+ // Pull the message off the port synchronously
829
+ return receiveMessageOnPort(this.#worker_port1).message;
830
+ }
831
+ }
832
+
833
+ //endregion
834
+
835
+ //region read-only Getters
836
+
837
+ /**
838
+ * Returns a detected or specified application root directory.
839
+ *
840
+ * @readonly
841
+ * @type {string}
842
+ */
843
+ get rootDirectory()
844
+ {
845
+ if(this.#root_dir !== undefined) return this.#root_dir;
846
+
847
+ // detect project directory
848
+ let dir = process.argv.find(arg => arg.startsWith("--root-dir="));
849
+
850
+ if(dir !== undefined)
851
+ {
852
+ dir = dir.substring(dir.indexOf("=") + 1).replace(/^"|"$/g, "");
853
+ }
854
+ else
855
+ {
856
+ dir = this.cwd();
857
+ }
858
+
859
+ while(true)
860
+ {
861
+ if(this.fileExists(path.join(dir, "package.json")) || this.fileExists(path.join(dir, "node_modules")) || this.fileExists(path.join(dir, "deno.json")))
862
+ {
863
+ this.#root_dir = dir;
864
+ return dir;
865
+ }
866
+ // go up one level
867
+ const parent = path.dirname(dir);
868
+ if(parent === dir) throw new Error(`${this._msg[8]} ${this.cwd()}`);
869
+ dir = parent;
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Gets the JavaScript runtime name ("node", "bun", "deno", or "unknown"). Useful when building an application compatible with multiple JavaScript runtimes.
875
+ * @type {string}
876
+ * @readonly
877
+ * @example <h2>Getting current JavaScript runtime:</h2>
878
+ * console.log(ui.runtime);
879
+ * // prints "node", or what ever your runtime is.
880
+ */
881
+ get runtime()
882
+ {
883
+ return this.#js_runtime;
884
+ }
885
+
886
+ /**
887
+ * Returns a short identifier of an operating system on current machine. Possible values: "windows", "linux", "darwin" (for MacOS), "freebsd", "openbsd", "sunos", and "android".
888
+ * @type {string}
889
+ * @readonly
890
+ * @example <h2>Getting a short identifier of an operating system:</h2>
891
+ * console.log(ui.os); // e.g., prints "windows"
892
+ */
893
+ get os()
894
+ {
895
+ let os = "";
896
+
897
+ if(this.runtime === "deno")
898
+ {
899
+ os = Deno.build.os;
900
+ }
901
+ else
902
+ {
903
+ os = process.platform;
904
+ }
905
+
906
+ if(os === "win32") os = "windows";
907
+
908
+ return os;
909
+ }
910
+
911
+ /**
912
+ * Getter property that returns an array of available UI look and feel IDs on your system. For example Windows OS can have a Windows look and feel that linux doesn't have. Use them to set the desired look and feel using ui.ui(id) method.
913
+ *
914
+ * @readonly
915
+ * @see {@link SwingUI#ui|ui.ui(id)}
916
+ * @type {string[]}
917
+ */
918
+ get list()
919
+ {
920
+ return /** @type {string[]} */ this.sendToGUI({java: "list"}, r =>
921
+ {
922
+ return r.list;
923
+ });
924
+ }
925
+
926
+ /**
927
+ * Returns an array of screen objects [{x,y,width,height}] that are configured on this computer. X and Y is the position of screens relative to one virtual screen. It's useful when you have multiple screens.
928
+ *
929
+ * @readonly
930
+ * @type {Object[]}
931
+ * @example <h2>Getting a list of screens:</h2>
932
+ * console.debug(ui.screens);
933
+ *
934
+ * // Example of returned array:
935
+ * [
936
+ * {
937
+ * default: true,
938
+ * id: '\\Display0',
939
+ * description: 'Win32GraphicsDevice[screen=0]',
940
+ * x: 0,
941
+ * y: 0,
942
+ * width: 2560,
943
+ * height: 1080,
944
+ * refresh_rate: 59,
945
+ * bit_depth: 32,
946
+ * type: 'screen'
947
+ * },
948
+ * {
949
+ * default: false,
950
+ * id: '\\Display1',
951
+ * description: 'Win32GraphicsDevice[screen=1]',
952
+ * x: -1280,
953
+ * y: 255,
954
+ * width: 1280,
955
+ * height: 1024,
956
+ * refresh_rate: 60,
957
+ * bit_depth: 32,
958
+ * type: 'screen'
959
+ * }
960
+ * ]
961
+ */
962
+ get screens()
963
+ {
964
+ return /** @type {Object[]} */ this.sendToGUI({system: "screens"}, r =>
965
+ {
966
+ return r.list;
967
+ });
968
+ }
969
+
970
+ /**
971
+ * Checks if current operating system supports system tray icons.
972
+ *
973
+ * @readonly
974
+ * @type {boolean}
975
+ */
976
+ get systemTraySupported()
977
+ {
978
+ return /** @type {boolean} */ this.sendToGUI({system: "systemTraySupported"}, r =>
979
+ {
980
+ return r.value;
981
+ });
982
+ }
983
+
984
+ /**
985
+ * Gets a GUI software license information stored in the package.json file of your project.
986
+ *
987
+ * @readonly
988
+ * @type {Object}
989
+ */
990
+ get license()
991
+ {
992
+ return /** @type {Object} */ this.sendToGUI({system: "getLicense"}, r =>
993
+ {
994
+ return r.value;
995
+ });
996
+ }
997
+
998
+ /**
999
+ * Gets and returns a swing-ui module version from package.json file.
1000
+ *
1001
+ * @readonly
1002
+ * @type {string}
1003
+ */
1004
+ get version()
1005
+ {
1006
+ try
1007
+ {
1008
+ return JSON.parse(this.readFile(`${this.#module_dir}/package.json`, "string", "utf8")).version;
1009
+ }catch(ex)
1010
+ {
1011
+ return "<unknown>";
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * Returns details of memory usage by GUI process.
1017
+ *
1018
+ * @readonly
1019
+ * @type {Object}
1020
+ * @example <h2>Getting RAM details about the GUI process:</h2>
1021
+ * console.log(ui.memoryUsage);
1022
+ *
1023
+ * // Example output
1024
+ * {
1025
+ * total: 268435456,
1026
+ * max: 4263510016,
1027
+ * max_heap: 4263510016,
1028
+ * used: 23540264,
1029
+ * non_heap: 39226904,
1030
+ * free: 244895192,
1031
+ * heap: 21443112
1032
+ * }
1033
+ */
1034
+ get memoryUsage()
1035
+ {
1036
+ return this.sendToGUI({java: "ram"}, r =>
1037
+ {
1038
+ return r.value;
1039
+ });
1040
+ }
1041
+
1042
+ /**
1043
+ * Returns an array of fonts installed on the system.
1044
+ *
1045
+ * @readonly
1046
+ * @type {string[]}
1047
+ * @example <h2>Getting a list of fonts installed on the system:</h2>
1048
+ * console.log(ui.fonts);
1049
+ *
1050
+ * // example output:
1051
+ * [
1052
+ * 'Adobe Arabic',
1053
+ * 'Adobe Caslon Pro',
1054
+ * 'Adobe Caslon Pro Bold',
1055
+ * ...
1056
+ * ]
1057
+ */
1058
+ get fonts()
1059
+ {
1060
+ return /** @type {string[]} */ this.sendToGUI({system: "fonts"}, r =>
1061
+ {
1062
+ return r.value;
1063
+ });
1064
+ }
1065
+
1066
+ /**
1067
+ * A getter to get environment variables.
1068
+ *
1069
+ * @readonly
1070
+ * @type {object}
1071
+ */
1072
+ get env()
1073
+ {
1074
+ return this.runtime === "deno" ? Deno.env.toObject() : process.env;
1075
+ }
1076
+
1077
+ /**
1078
+ * Gets a current process ID
1079
+ *
1080
+ * @readonly
1081
+ * @type {number}
1082
+ */
1083
+ get pid()
1084
+ {
1085
+ return this.runtime === "deno" ? Deno.pid : process.pid;
1086
+ }
1087
+
1088
+ //endregion
1089
+
1090
+ //region GUI
1091
+
1092
+ /**
1093
+ * Performs garbage collection in JavaScript and GUI.
1094
+ * @return {SwingUI} Returns an instance of SwingUI object for method chaining.
1095
+ * @example <h2>Usage example:</h2>
1096
+ * ui.gc();
1097
+ */
1098
+ gc()
1099
+ {
1100
+ if(global.gc) global.gc(); // Trigger garbage collection (requires Node.js started with --expose-gc)
1101
+ this.sendToGUI({java: "gc"}); // Trigger garbage collection in GUI
1102
+ return this;
1103
+ }
1104
+
1105
+ /**
1106
+ * Increases and decreases default font size of all UI components.
1107
+ *
1108
+ * @param {number} by - By how many pixels to adjust font size (positive integer increases size, negative decreases)
1109
+ * @return {this} Returns an instance of SwingUI object for method chaining.
1110
+ */
1111
+ adjustFontSize(by)
1112
+ {
1113
+ this.sendToGUI({java: "adjustFontSize", value: parseInt(by)});
1114
+ return this;
1115
+ }
1116
+
1117
+ /**
1118
+ * Increases and decreases default font size of all controls.
1119
+ *
1120
+ * @param {number} by - By how much to adjust font size (positive increases size, negative decreases)
1121
+ * @return {this} Returns an instance of SwingUI object for method chaining.
1122
+ */
1123
+ adjustDefaultFontSize(by)
1124
+ {
1125
+ this.sendToGUI({java: "adjustDefaultFontSize", value: parseInt(by)});
1126
+ return this;
1127
+ }
1128
+
1129
+ /**
1130
+ * Sets default font to be used by all components. Any font property may be changed (name, size, bold, etc.)
1131
+ *
1132
+ * @param {Font} font - New font
1133
+ * @returns {this} Returns an instance of SwingUI object for method chaining.
1134
+ */
1135
+ setDefaultFont(font)
1136
+ {
1137
+ if(typeof font !== "object") throw Error("Font argument must be an object of Font type");
1138
+ this.sendToGUI({java: "defaultFont", value: font});
1139
+ return this;
1140
+ }
1141
+
1142
+ /**
1143
+ * Sets or reads clipboard content data. Data types must be values of object (enum) DataType.
1144
+ *
1145
+ * @param {object} [data] - Object ob data to be stored in a clipboard {Type: data}. See example below.
1146
+ * @returns {object|SwingUI} When setting arguments, `this` instance of SwingUI is returned, otherwise clipboard data object is returned.
1147
+ * @see DataType
1148
+ * @example <h2>Valid data types for the clipboard data object:</h2>
1149
+ * // Sets all possible data types to clipboard
1150
+ * ui.clipboard({
1151
+ * Text: "text to copy",
1152
+ * HTML: "<b>HTML Content</b>",
1153
+ * JSON: object,
1154
+ * Image: "base64 encoded image data",
1155
+ * URIList: ["http://url_1.com", "http://url_2.com"],
1156
+ * Files: ["C://my_file.txt", ...],
1157
+ * URL: "http://url.com"
1158
+ * });
1159
+ * @example <h2>Setting some plain text and HTML data to clipboard:</h2>
1160
+ * const data = {};
1161
+ * data[DataType.Text] = "Plain text content";
1162
+ * data[DataType.HTML] = "<b>HTML Content</b>";
1163
+ * ui.clipboard(data);
1164
+ */
1165
+ clipboard()
1166
+ {
1167
+ if(arguments.length > 0)
1168
+ {
1169
+ this.sendToGUI({
1170
+ system: "clipboard",
1171
+ value: (typeof arguments[0] === "string") ? {Text: arguments[0]} : arguments[0]
1172
+ });
1173
+ return this;
1174
+ }
1175
+ else
1176
+ {
1177
+ return this.sendToGUI({system: "clipboard"}, r =>
1178
+ {
1179
+ return r.value;
1180
+ });
1181
+ }
1182
+ }
1183
+
1184
+ /**
1185
+ * Sets or gets boolean whether the layout of containers is validated during resizing, or after resizing is complete.
1186
+ *
1187
+ * @param {boolean} [value] - Enables or disables dynamic layout
1188
+ * @return {boolean|SwingUI} When setting arguments, `this` instance of SwingUI is returned, otherwise current value is returned.
1189
+ */
1190
+ dynamicLayout(...args)
1191
+ {
1192
+ if(args.length > 0)
1193
+ {
1194
+ this.sendToGUI({java: "dynamicLayout", value: (args[0] === true)});
1195
+ return this;
1196
+ }
1197
+ else
1198
+ {
1199
+ return this.sendToGUI({java: "dynamicLayout"}, r =>
1200
+ {
1201
+ return r.value;
1202
+ });
1203
+ }
1204
+ }
1205
+
1206
+ /**
1207
+ * Gets or sets Java's GUI look and feel ID. Obtain a list of supported UIs by the system using ui.list getter property.
1208
+ *
1209
+ * @param {string} [id] - Identifies of Java Swing's UI to be used.
1210
+ * @param {object} [options] - Additional look-and-feel options. Options apply only to some look-and-feels, like FlatLaf. Example: {border-radius: 5}.
1211
+ * @return {string|SwingUI} When setting arguments, `this` instance of SwingUI is returned, otherwise current value is returned.
1212
+ * @throws Error When argument is invalid.
1213
+ * @see SwingUI#list
1214
+ * @example <h2>Setting FlatDarkLaf UI with all options</h2>
1215
+ * ui.ui(
1216
+ * "FlatDarkLaf",
1217
+ * {
1218
+ * borderRadius:8,
1219
+ * showScrollbarButtons: true,
1220
+ * scrollbarSize: 14,
1221
+ * selectedTabColor: "#FFFFFF",
1222
+ * scrollbarColor: "#FF7777",
1223
+ * arrowType: "triangle" // "triangle" or "chevron" (default)
1224
+ * }
1225
+ * );
1226
+ */
1227
+ ui(...args)
1228
+ {
1229
+ if(args.length > 0)
1230
+ {
1231
+ const e = this.sendToGUI({
1232
+ java: "ui",
1233
+ value: {id: args[0], options: args[1] ?? undefined}
1234
+ }, r => {return r.value;});
1235
+ if(e === false) throw new Error(`Failed to set UI '${args[0]}'.`);
1236
+ queueMicrotask(() => {this.#processEvent(e);});
1237
+ return this;
1238
+ }
1239
+ else
1240
+ {
1241
+ return /** @type {string} */ this.sendToGUI({java: "ui"}, r =>
1242
+ {
1243
+ return r.value;
1244
+ });
1245
+ }
1246
+ }
1247
+
1248
+ /**
1249
+ * Shows a message dialog window like alert() in web browser.
1250
+ *
1251
+ * @param {string} message
1252
+ * @param {string} [title] - Message window title
1253
+ * @param {MessageIcon} [icon] - Use: MessageIcon.[Info|Warning|Error]
1254
+ * @param {Window} [ownerWindow] - If set, the message dialog window will attempts to be centered at specified window
1255
+ * @return {void} Does not return a value.
1256
+ */
1257
+ showMessage(message, title = "Message", icon = MessageIcon.Info, ownerWindow = null)
1258
+ {
1259
+ if(typeof message !== "string") message = stringify(message);
1260
+ ownerWindow = (ownerWindow instanceof globalThis.Window) ? ownerWindow.id : null;
1261
+ this.sendToGUI({
1262
+ java: "showMessage",
1263
+ message: message,
1264
+ title: String(title),
1265
+ icon: parseInt(icon),
1266
+ owner: ownerWindow
1267
+ }, function(r)
1268
+ {
1269
+ return r.value;
1270
+ });
1271
+ }
1272
+
1273
+ /**
1274
+ * Sets a custom Error handler when error events come if from GUI. Event object has a "e.message" parameter, and
1275
+ * may have "e.target" that references a component related to the error.
1276
+ * e:{
1277
+ * message (always)
1278
+ * target: (when component is related to the error)
1279
+ * }
1280
+ * @param {function(Object):any} callback - Function to be called instead of default error handling.
1281
+ * @return {SwingUI} Returns `this` instance of SwingUI class.
1282
+ */
1283
+ onError(callback)
1284
+ {
1285
+ this.callback.onError = callback;
1286
+ return this;
1287
+ }
1288
+
1289
+ /**
1290
+ * Gets or sets text rendering mode used in UI components.
1291
+ *
1292
+ * @param {string} [mode] - Specifies font antialiasing mode to use (on|lcd|gasp|off) (NOT IMPLEMENTED YET).
1293
+ * @returns {string|null} Returns font antialiasing mode: `on`, `lcd`, `gasp`, `off`, or `null` object if font antialiasing could not be detected.
1294
+ */
1295
+ fontAntialiasing(...args)
1296
+ {
1297
+ if(args.length > 0)
1298
+ {
1299
+ throw new Error(`Settings text rendering mode is not implemented yet.`);
1300
+ }
1301
+ else
1302
+ {
1303
+ return this.sendToGUI({system: "fontAntialiasing"}, r => {return r.value;});
1304
+ }
1305
+ }
1306
+
1307
+ /**
1308
+ * Gets or sets the event throttle interval for high-frequency events, like mouse motion or resizing. Value is in milliseconds, so 100 means no more than 10 same type of events per component per second.
1309
+ * Applies only to some high frequency events, such as resizing and moving.
1310
+ *
1311
+ * @param {number} [ms] - Minimum amount of time (in milliseconds) between same type of events per component.
1312
+ * @return {SwingUI|number} When setting arguments, `this` instance of SwingUI is returned, otherwise current value is returned.
1313
+ * @example <h2>Setting and getting examples:</h2>
1314
+ * ui.eventThrottle(10); // Sets new value
1315
+ * console.debug(ui.eventThrottle()); // prints current value
1316
+ */
1317
+ eventThrottle(...args)
1318
+ {
1319
+ if(args.length > 0)
1320
+ {
1321
+ this.sendToGUI({java: "eventThrottle", value: parseInt(args[0])});
1322
+ return this;
1323
+ }
1324
+ else
1325
+ {
1326
+ return /** @type {number} */ this.sendToGUI({java: "eventThrottle"}, r =>
1327
+ {
1328
+ return r.value;
1329
+ });
1330
+ }
1331
+ }
1332
+
1333
+ /**
1334
+ * Adjusts or retrieves the thread pool size and keep-alive time for image loading tasks.
1335
+ * Each argument is optional. Setting `null` to any argument will not change it's value.
1336
+ * Returns current configuration if no arguments are specified.
1337
+ *
1338
+ * @param {number} [min] - The minimum number of threads to keep alive (core pool size). Default is 1.
1339
+ * @param {number} [max] - The maximum number of threads allowed (max pool size). Default is 5.
1340
+ * @param {number} [keep_alive] - The idle time (in seconds) before excess threads are terminated when idle. Default is 5 seconds.
1341
+ * @return {object|SwingUI} When setting arguments, `this` instance of SwingUI is returned, otherwise current value is returned.
1342
+ */
1343
+ iconLoadingThreads(...args)
1344
+ {
1345
+ if(args.length === 0)
1346
+ {
1347
+ return this.sendToGUI({java: "iconLoadingThreads"}, r =>
1348
+ {
1349
+ return r.value;
1350
+ });
1351
+ }
1352
+ else
1353
+ {
1354
+ const opt = {min: parseInt(args[0])};
1355
+ if(args.length > 1) opt.max = parseInt(args[1]);
1356
+ if(args.length > 2) opt.keep_alive = parseInt(args[2]);
1357
+
1358
+ this.sendToGUI({java: "iconLoadingThreads", ...opt});
1359
+ return this;
1360
+ }
1361
+ }
1362
+
1363
+ /**
1364
+ * Requests operating system to move top-most window of this application (or all windows) to foreground before other windows. Operating may refuse to do it.
1365
+ * To forcefully bring a specific Window to the front you can set window.alwaysOnTop(true), and then window.alwaysOnTop(false), but it should not be abused in order to maintain a positive user experience.
1366
+ *
1367
+ * @param {boolean} [all_windows=false] - Move all windows of this application to foreground instead of just top-most window.
1368
+ * @returns {SwingUI} returns `this` ui object for chaining.
1369
+ */
1370
+ requestForeground(all_windows = false)
1371
+ {
1372
+ this.sendToGUI({system: "requestForeground", value: all_windows === true});
1373
+ return this;
1374
+ }
1375
+
1376
+ /**
1377
+ * Sets or gets a default menu bar that should be used by macOS at the top of the screen. If the specified menu bar was added to any parent component, it will be removed from that component.
1378
+ * This is supported only on macOS.
1379
+ *
1380
+ * @param {MenuBar|null} [menu_bar] - MenuBar component. Set to `null` to remove default menu bar.
1381
+ * @return {MenuBar|boolean} If called without parameters, a default `Menubar` component is returned or `null` of it was never set; otherwise `boolean` is returned indicating if the operation was accepted by the operating system.
1382
+ * @throws {Error} When passed argument is not a {@link MenuBar} object.
1383
+ */
1384
+ defaultMenuBar(...args)
1385
+ {
1386
+ if(args[0] != null && !(args[0] instanceof MenuBar)) throw new Error(`Invalid argument in ui.defaultMenuBar(menu_bar). Expected 'MenuBar' object, but received ${typeof args[0]}.`);
1387
+
1388
+ if(args.length > 0)
1389
+ {
1390
+ if(args[0] != null) args[0].remove(); // remove from previous parents (or else GUI will anyway).
1391
+
1392
+ return this.sendToGUI({
1393
+ system: "defaultMenuBar",
1394
+ value: args[0] instanceof MenuBar ? args[0].id : null
1395
+ }, r =>
1396
+ {
1397
+ return r.value;
1398
+ });
1399
+ }
1400
+ else
1401
+ {
1402
+ return this.sendToGUI({system: "defaultMenuBar"}, r =>
1403
+ {
1404
+ return r.value;
1405
+ });
1406
+ }
1407
+ }
1408
+
1409
+ /**
1410
+ * Gracefully exits the application with specified exit code (status code) and optional exit signal. Emits onExit() event handlers, closes connection to GUI and notifies GUI to gracefully exit. This is a recommended way to exit the application.
1411
+ *
1412
+ * @param {number} [exitCode] - Exit code.
1413
+ * @param {string} [signal] - One of received exit signals: `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`, `SIGHUP`.
1414
+ * @return {void} Does not return a value.
1415
+ * @example <h2>Exiting the app gracefully:</h2>
1416
+ * ui.exit();
1417
+ * ui.exit(10); // existing with exit code 10
1418
+ * @see SwingUI#onExit
1419
+ */
1420
+ exit(exitCode = 0, signal = undefined)
1421
+ {
1422
+ if(this.#exiting) return;
1423
+ this.#exiting = true;
1424
+
1425
+ // emit the event
1426
+ const e = {event: "onExit", type: "exit", id: 0, code: exitCode};
1427
+ if(signal !== undefined) e.signal = signal;
1428
+ this.#processEvent(e);
1429
+
1430
+ this.#exitJava(exitCode);
1431
+ clearInterval(this.#watchdog_timer);
1432
+
1433
+ setTimeout(() =>
1434
+ {
1435
+ if(ui.runtime === "deno")
1436
+ {
1437
+ Deno.exit(exitCode);
1438
+ }
1439
+ else
1440
+ {
1441
+ process.exit(exitCode);
1442
+ }
1443
+ }, 100); // Giving some time for Java to receive the signal and process cleaning
1444
+ }
1445
+
1446
+ #exitJava(exitCode = 0)
1447
+ {
1448
+ this.sendToGUI({java: "exit", code: exitCode});
1449
+
1450
+ try
1451
+ {
1452
+ //this.#jvm.eventListener.reset();
1453
+ }catch(_)
1454
+ {
1455
+ }
1456
+ try
1457
+ {
1458
+ this.#jvm.stdio.guard.release();
1459
+ }catch(_)
1460
+ {
1461
+ }
1462
+
1463
+ setTimeout(() =>
1464
+ {
1465
+ if(this.#guiManagerWorker != null)
1466
+ {
1467
+ this.#guiManagerWorker.postMessage({
1468
+ type: "__shutdown",
1469
+ code: exitCode
1470
+ });
1471
+ }
1472
+
1473
+ }, 10);
1474
+ }
1475
+
1476
+ //endregion
1477
+
1478
+ //region Utility Functions
1479
+
1480
+ /**
1481
+ * Sets and gets locking keys state (Caps lock key, Num lock key, and Scroll lock key) to ON and OFF.
1482
+ *
1483
+ * @param {Object} [keys] - Keys object example {caps_lock: false, num_lock: true, scroll_lock: false}. Only set keys you want to change.
1484
+ * @return {Object|SwingUI} When setting arguments, `this` instance of SwingUI is returned, otherwise current value is returned.
1485
+ * @example <h2>Setting Caps Lock to ON</h2>
1486
+ * ui.lockingKeys({caps_lock: true});
1487
+ */
1488
+ lockingKeys()
1489
+ {
1490
+ if(arguments.length > 0)
1491
+ {
1492
+ this.sendToGUI({system: "lockingKeys", value: arguments[0]});
1493
+ return this;
1494
+ }
1495
+ else
1496
+ {
1497
+ return this.sendToGUI({system: "lockingKeys"}, r =>
1498
+ {
1499
+ return r.value;
1500
+ });
1501
+ }
1502
+ }
1503
+
1504
+ /**
1505
+ * Gets or sets a position of a pointing device. If "screen" is specified,
1506
+ * the {x,y} position will be relative to the specified screen, otherwise
1507
+ * the position will be set relative to screen at index 0 (default screen).
1508
+ * You can get a list of screens with ui.screens getter.
1509
+ *
1510
+ * @param {number} [x] - Use only to set pointer location
1511
+ * @param {number} [y] - required if x is specified
1512
+ * @param {number} [screen]
1513
+ * @return {Object | SwingUI} When setting arguments, `this` instance of SwingUI is returned, otherwise current value is returned.
1514
+ */
1515
+ mouseLocation()
1516
+ {
1517
+ if(arguments.length > 1)
1518
+ {
1519
+ this.sendToGUI({
1520
+ system: "mouseLocation",
1521
+ value: {
1522
+ x: parseInt(arguments[0]),
1523
+ y: parseInt(arguments[1]),
1524
+ screen: arguments.length > 2 ? Math.abs(parseInt(arguments[2])) : undefined // don't set default screen index here, it may not be 0
1525
+ }
1526
+ });
1527
+ this.sendMessageQueue(); // important, helps for mouse event to be detected
1528
+ return this;
1529
+ }
1530
+ else
1531
+ {
1532
+ return this.sendToGUI({system: "mouseLocation"}, r =>
1533
+ {
1534
+ return r.value;
1535
+ });
1536
+ }
1537
+ }
1538
+
1539
+ /**
1540
+ * Performs mouse button action or mouse wheel scrolling action.
1541
+ *
1542
+ * @param {MouseAction} action - MouseAction.[Press, Release, Click, DoubleClick, VerticalScroll, HorizontalScroll]
1543
+ * @param {MouseButton|number} button_or_scrolls - MouseButton.[Left, Right, Middle] OR integer for scroll amount if action is MouseAction.VerticalScroll or MouseAction.HorizontalScroll
1544
+ * @return {SwingUI} Returns `this` instance of SwingUI class.
1545
+ * @see MouseAction
1546
+ */
1547
+ mouseAction(action, button_or_scrolls)
1548
+ {
1549
+ let data = {action: parseInt(action)};
1550
+
1551
+ if(data.action === MouseAction.VerticalScroll || data.action === MouseAction.HorizontalScroll)
1552
+ {
1553
+ data.scroll = button_or_scrolls;
1554
+ }
1555
+ else
1556
+ {
1557
+ data.button = button_or_scrolls;
1558
+ }
1559
+
1560
+ this.sendToGUI({system: "mouseAction", value: data});
1561
+ return this;
1562
+ }
1563
+
1564
+ /**
1565
+ * Performs a key press, key release, or key type operation. For value use one character string when using "Press" or "Release" action, or a string of text to be typed when using "Type" action.
1566
+ *
1567
+ * @param {KeyAction} action - KeyAction.[Press|Release|Type]
1568
+ * @param {number|number[]|string} key_code_or_string - Key code, array of key codes, or a string, which will be converted to an array of key codes.
1569
+ * @return {SwingUI} Returns `this` instance of SwingUI class.
1570
+ * @see KeyAction
1571
+ * @example <h2>Performing a key type action:</h2>
1572
+ * ui.keyboardAction(KeyAction.Type, "A");
1573
+ */
1574
+ keyboardAction(action, key_code_or_string)
1575
+ {
1576
+ if(arguments.length < 2) return this;
1577
+
1578
+ let data = {action: action};
1579
+
1580
+ if(typeof key_code_or_string === "number")
1581
+ {
1582
+ data.keyCodes = [key_code_or_string];
1583
+ }
1584
+ else if(Array.isArray(key_code_or_string))
1585
+ {
1586
+ data.keyCodes = key_code_or_string;
1587
+ }
1588
+ else if(typeof key_code_or_string === "string")
1589
+ {
1590
+ data.text = key_code_or_string;
1591
+ }
1592
+
1593
+ this.sendToGUI({system: "keyboardAction", value: data});
1594
+ return this;
1595
+ }
1596
+
1597
+ /**
1598
+ * Returns the index of the screen whose bounds contain the given coordinate, or -1 if the point does not fall within any configured screen.
1599
+ *
1600
+ * Accepts either:
1601
+ * - Separate numeric coordinates (x, y), or
1602
+ * - A point-like object with numeric `x` and `y` properties.
1603
+ *
1604
+ * @param {number|Object} xOrPoint - Either the X coordinate or a point object containing {x, y}.
1605
+ * @param {number} [y] - The Y coordinate (required if xOrPoint is a number).
1606
+ * @returns {number} The index of the matching screen, or -1 if none matched.
1607
+ * @throws {Error} If the input is invalid (object is missing `x` or `y` properties) or coordinates are not finite numbers.
1608
+ *
1609
+ * @example <h2>Using separate x and y coordinates</h2>
1610
+ * const screenIndex = ui.screenIndexAt(1920, 540);
1611
+ * console.log(screenIndex); // e.g. 1, or -1 if not within any screen
1612
+ *
1613
+ * @example <h2>Using a point object</h2>
1614
+ * const cursor = { x: 3440, y: 1080 };
1615
+ * const screenIndex = ui.screenIndexAt(cursor);
1616
+ * console.log(screenIndex); // e.g. 2, or -1 if not within any screen
1617
+ */
1618
+ screenIndexAt(xOrPoint, y = undefined)
1619
+ {
1620
+ let px, py;
1621
+
1622
+ if(typeof xOrPoint === 'object')
1623
+ {
1624
+ if(xOrPoint == null || !("x" in xOrPoint) || !("y" in xOrPoint)) throw new Error(`Invalid point object. Point object must be an object with "x" and "y" integer properties.`);
1625
+ px = xOrPoint.x;
1626
+ py = xOrPoint.y;
1627
+ }
1628
+ else
1629
+ {
1630
+ px = xOrPoint;
1631
+ py = y;
1632
+ }
1633
+
1634
+ if(!Number.isFinite(px) || !Number.isFinite(py)) throw new Error(`Invalid x or y arguments.`);
1635
+
1636
+ return this.screens.findIndex(s => px >= s.x && px < s.x + s.width && py >= s.y && py < s.y + s.height);
1637
+ }
1638
+
1639
+ /**
1640
+ * Opens provided URL in a default web browser.
1641
+ *
1642
+ * @param {string} url - Website address to open.
1643
+ * @return {this} Returns `this` instance of SwingUI class.
1644
+ */
1645
+ openURL(url)
1646
+ {
1647
+ this.sendToGUI({system: "openURL", value: url});
1648
+ return this;
1649
+ }
1650
+
1651
+ /**
1652
+ * Opens file or a folder with a default application associated the file type, or inside a file manager application (is specified).
1653
+ *
1654
+ * @param {string} path - Absolute path of a local file or directory.
1655
+ * @param {boolean} [in_file_manager=false] - Indicates if the file should be open inside a default file manager application instead of a default application associated with that file type.
1656
+ * @return {boolean} Returns `true` if the path exists and this operation is supported by the operating system; otherwise returns `false`.
1657
+ */
1658
+ openPath(path, in_file_manager = false)
1659
+ {
1660
+ return this.sendToGUI({system: "openPath", value: {path: path, in_folder: in_file_manager}}, r =>
1661
+ {
1662
+ return r.value;
1663
+ });
1664
+ }
1665
+
1666
+ /**
1667
+ * Tells operating system to print a file using default file printing dialog window.
1668
+ *
1669
+ * @param {String} path - Absolute path of a local file to print.
1670
+ * @return {boolean} Returns `true` if the file exists and this operation is supported by the operating system; otherwise returns `false`.
1671
+ */
1672
+ printFile(path)
1673
+ {
1674
+ return this.sendToGUI({system: "print", value: path}, r =>
1675
+ {
1676
+ return r.value;
1677
+ });
1678
+ }
1679
+
1680
+ /**
1681
+ * Opens the default mail client’s compose window with fields pre-filled according to a `mailto:` URI.
1682
+ * The `mailto:` URI can include message fields such as `to`, `cc`, `subject`, `body`, etc.
1683
+ * For details, see [RFC 2368](https://www.rfc-editor.org/rfc/rfc2368) (The mailto URL scheme).
1684
+ * In some cases an error message may provide details as to why starting the application failed.
1685
+ *
1686
+ * @param {string} [mailto_uri=""] - A `mailto:` URI. Leave empty to simply open the compose window without any fields filled in.
1687
+ * @returns {boolean} Returns `true` if this action is supported by the operating system and the default mail application compose window starts successfully; otherwise returns `false`.
1688
+ */
1689
+ mail(mailto_uri = "")
1690
+ {
1691
+ return this.sendToGUI({system: "mail", value: mailto_uri}, r =>
1692
+ {
1693
+ return r.value;
1694
+ });
1695
+ }
1696
+
1697
+ /**
1698
+ * Synchronously pauses main thread code execution for a specified amount of milliseconds.
1699
+ *
1700
+ * @param {number} ms - Amount of milliseconds to sleep.
1701
+ * @returns {boolean} Returns `true` when code execution resumed because of specified timeout, `false` for any other reason.
1702
+ */
1703
+ sleep(ms)
1704
+ {
1705
+ return (Atomics.wait(this.#sleepBuffer, 0, 0, ms) === "timed-out");
1706
+ }
1707
+
1708
+ //endregion
1709
+
1710
+ //region File System Methods
1711
+
1712
+ /**
1713
+ * Synchronously checks if the file or directory exist.
1714
+ *
1715
+ * @param {string|URL} file_path - File or directory path to check
1716
+ * @returns {boolean} Returns `true` if the file or directory exist; otherwise returns `false`.
1717
+ * @throws {Error} When the file path is invalid.
1718
+ */
1719
+ fileExists(file_path)
1720
+ {
1721
+ if(!(typeof file_path === "string")) throw new Error(`Invalid "file_path" argument: ${typeof file_path}`);
1722
+
1723
+ try
1724
+ {
1725
+ return (this.runtime === "deno") ? (Deno.statSync(file_path) != null) : fs.existsSync(file_path);
1726
+ }catch(ex)
1727
+ {
1728
+ if(ex instanceof Deno.errors.NotFound) return false;
1729
+ throw ex;
1730
+ }
1731
+ }
1732
+
1733
+ /**
1734
+ * Synchronously reads a local file and returns its data.
1735
+ * NOTE: Memory limit set by JavaScript runtime and operating system limits the file size that can be read.
1736
+ *
1737
+ * @param {string} file_path
1738
+ * @param {string} [data_type="string"] - Desired return data type: `string`, `Uint8Array`, `ArrayBuffer`, `Buffer`. NOTE: `Buffer` may not be supported in all JavaScript runtimes and exception may occur.
1739
+ * @param {string} [encoding="utf-8"] - Text encoding to use. Only applies when `data_type` argument is "string".
1740
+ * @returns {string|Uint8Array|ArrayBuffer|Buffer} Returns the data in specified data type.
1741
+ * @throws {Error} When the file path is invalid.
1742
+ * @throws {Error} When file doesn't exist.
1743
+ * @throws {Error} When file cannot be read.
1744
+ * @throws {Error} When `data_type` parameter is invalid.
1745
+ */
1746
+ readFile(file_path, data_type = "string", encoding = "utf-8")
1747
+ {
1748
+ if(!(typeof file_path === "string")) throw new Error(`Invalid "file_path" argument: ${typeof file_path}`);
1749
+
1750
+ if(!this.fileExists(file_path)) throw new Error(`File not found: ${file_path}`);
1751
+
1752
+ if(!this.canReadFile(file_path)) throw new Error(`Failed to read file: Access Denied`);
1753
+
1754
+ let data = null; // originally should start with Uint8Array
1755
+
1756
+ try
1757
+ {
1758
+ if(this.runtime === "deno" && Deno.readFileSync)
1759
+ {
1760
+ data = Deno.readFileSync(file_path);
1761
+ }
1762
+ else
1763
+ {
1764
+ data = fs.readFileSync(file_path);
1765
+ }
1766
+ }catch(ex)
1767
+ {
1768
+ throw new Error(`Failed to read file: ${ex.message}`);
1769
+ }
1770
+
1771
+ switch(data_type)
1772
+ {
1773
+ case "string":
1774
+ {
1775
+ return new TextDecoder(encoding).decode(data);
1776
+ }
1777
+ case "Uint8Array":
1778
+ {
1779
+ return (Buffer !== undefined && data instanceof Buffer) ? new Uint8Array(data) : data;
1780
+ }
1781
+ case "ArrayBuffer":
1782
+ {
1783
+ return (Buffer !== undefined && data instanceof Buffer) ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : data.buffer;
1784
+ }
1785
+ case "Buffer":
1786
+ {
1787
+ return Buffer.from(data);
1788
+ }
1789
+ default:
1790
+ {
1791
+ throw new Error(`Invalid "format" argument specified: ${data_type}. Expected "string", "Uint8Array", "ArrayBuffer", or "Buffer".`);
1792
+ }
1793
+ }
1794
+ }
1795
+
1796
+ /**
1797
+ * Writes data to a file synchronously.
1798
+ *
1799
+ * @param {string|URL} file_path - Local destination file path.
1800
+ * @param {string|ArrayBuffer|Uint8Array|Buffer} data - Data to write to a file.
1801
+ * @param {string} [mode="write"] - "write" or "append"
1802
+ * @param {boolean} [overwrite=true] - `true` to overwrite existing file with the same path. `false` to fail and return `false` if a file with the same path already exists.
1803
+ * @param {boolean} [create_dir=false] - `true` to create directory if it doesn't exist. `false` is default. If `false` and directory doesn't exist, an Errow will be thrown.
1804
+ * @returns {boolean} Returns `true` on success, or throws an Error on failure.
1805
+ * @throws {Error} When the file path is invalid.
1806
+ * @throws {Error} When the file exists and `overwrite` argument is not `true`.
1807
+ * @throws {Error} When access to the file is denied.
1808
+ * @throws {Error} When directory to the specified file failed to be automatically created.
1809
+ * @throws {Error} When the directory of the file path doesn't exist and `create_dir` argument is not true.
1810
+ * @throws {Error} When failed to write the file.
1811
+ */
1812
+ writeFile(file_path, data, mode = "write", overwrite = true, create_dir = false)
1813
+ {
1814
+ // validate path type
1815
+ if(!(typeof file_path === "string")) throw new Error(`Invalid "file_path" argument: ${typeof file_path}`);
1816
+
1817
+ // validate data type
1818
+ if(!(typeof data === "string") && !(data instanceof ArrayBuffer) && !(data instanceof Uint8Array) && !(Buffer !== undefined && data instanceof Buffer)) throw new Error(`Invalid "data" argument type: ${typeof file_path}. Expected string, ArrayBuffer, Buffer or Uint8Array.`);
1819
+
1820
+ if(overwrite !== true && this.fileExists(file_path)) throw new Error(`File already exists. To overwrite the file set "overwrite" argument to true.`);
1821
+
1822
+ if(mode === "append" && this.fileExists(file_path) && !this.canWriteFile(file_path)) throw new Error(`Failed to write file: Access Denied`);
1823
+
1824
+ if(typeof data === "string") data = new TextEncoder().encode(data); // convert string to Uint8Array
1825
+
1826
+ try
1827
+ {
1828
+ if(!ui.fileExists(path.dirname(file_path)))
1829
+ {
1830
+ if(create_dir)
1831
+ {
1832
+ try
1833
+ {
1834
+ ui.makeDirectory(path.dirname(file_path), true);
1835
+ }catch(ex)
1836
+ {
1837
+ throw new Error(`Failed to create a directory that doesn't exist: ${path.dirname(file_path)}:\n${ex.message}`);
1838
+ }
1839
+ }
1840
+ else
1841
+ {
1842
+ throw new Error(`Directory doesn't exist: ${path.dirname(file_path)}`);
1843
+ }
1844
+ }
1845
+
1846
+ if(this.runtime === "deno")
1847
+ {
1848
+ Deno.writeFileSync(file_path, (data instanceof Uint8Array) ? data : new Uint8Array(data), {append: mode === "append"});
1849
+ }
1850
+ else
1851
+ {
1852
+ const flag = mode === "append" ? "a" : "w";
1853
+ fs.writeFileSync(file_path, (data instanceof ArrayBuffer) ? Buffer.from(data) : data, {flag: flag});
1854
+ }
1855
+
1856
+ return true;
1857
+ }catch(err)
1858
+ {
1859
+ throw new Error(`Failed to write file ${file_path}: \n\nError: ${err.message}`);
1860
+ }
1861
+ }
1862
+
1863
+ /**
1864
+ * Gets information about a file or directory, like times, size, if it's a file or directory, etc.
1865
+ *
1866
+ * @param {string|URL} file_path - Local file or directory path.
1867
+ * @param {boolean} [about_link=false] - `true` makes the method return information about the symbolic link itself instead of the path the link redirects to. Only applies to symbolic links.
1868
+ * @returns {FileStat} Returns information about specified file or directory, or throws an Error if it doesn't exist, access is denied, or argument is invalid.
1869
+ * @throws {Error} When the file path is invalid.
1870
+ * @throws {Error} When the file is not found.
1871
+ * @see FileStat
1872
+ */
1873
+ fileStat(file_path, about_link = false)
1874
+ {
1875
+ if(!(typeof file_path === "string")) throw new Error(`Invalid "file_path" argument: ${typeof file_path}`);
1876
+ if(!this.fileExists(file_path)) throw new Error(`File not found: ${file_path}`);
1877
+
1878
+ let stat = null;
1879
+
1880
+ if(about_link)
1881
+ {
1882
+ stat = (this.runtime === "deno") ? Deno.lstatSync(file_path) : fs.lstatSync(file_path);
1883
+ }
1884
+ else
1885
+ {
1886
+ stat = (this.runtime === "deno") ? Deno.statSync(file_path) : fs.statSync(file_path);
1887
+ }
1888
+
1889
+ return {
1890
+ isFile: (typeof stat.isFile === "function") ? stat.isFile() : stat.isFile,
1891
+ isDirectory: (typeof stat.isDirectory === "function") ? stat.isDirectory() : stat.isDirectory,
1892
+ isSymlink: (typeof stat.isSymbolicLink === "function") ? stat.isSymbolicLink() : stat.isSymlink,
1893
+ dev: stat.dev,
1894
+ rdev: stat.rdev,
1895
+ nlink: stat.nlink,
1896
+ user: stat.uid,
1897
+ group: stat.gid,
1898
+ size: stat.size,
1899
+ mode: stat.mode,
1900
+ atime: stat.atime,
1901
+ mtime: stat.mtime,
1902
+ ctime: stat.ctime,
1903
+ birthtime: stat.birthtime
1904
+ };
1905
+ }
1906
+
1907
+ /**
1908
+ * Deletes a file or moves it to trash synchronously.
1909
+ *
1910
+ * @param {string} file_path - Path to the file to delete
1911
+ * @param {boolean} [move_to_trash=false] - Move file to trash instead of completely deleting it.
1912
+ * @returns {boolean} - Returns `true` on success or if the file didn't exist; otherwise returns `false` on failure
1913
+ * @throws {Error} When the file path is invalid.
1914
+ * @throws {Error} When the file path is a directory.
1915
+ */
1916
+ deleteFile(file_path, move_to_trash = false)
1917
+ {
1918
+ if(!(typeof file_path === "string")) throw new Error(`Invalid "file_path" argument: ${typeof file_path}`);
1919
+ if(!this.fileExists(file_path)) return true;
1920
+ if(!this.fileStat(file_path).isFile) throw new Error(`Failed to delete file: Provided path is a directory. Use ui.deleteDirectory() instead.`);
1921
+
1922
+ if(move_to_trash)
1923
+ {
1924
+ return this.sendToGUI({system: "moveToTrash", value: file_path}, r =>
1925
+ {
1926
+ return r.value;
1927
+ });
1928
+ }
1929
+
1930
+ try
1931
+ {
1932
+ if(this.runtime === "deno")
1933
+ {
1934
+ Deno.removeSync(file_path);
1935
+ }
1936
+ else
1937
+ {
1938
+ fs.unlinkSync(file_path);
1939
+ }
1940
+ return true;
1941
+ }catch(err)
1942
+ {
1943
+ throw err;
1944
+ }
1945
+ }
1946
+
1947
+ /**
1948
+ * Deletes a directory synchronously.
1949
+ *
1950
+ * @param {string} dir_path - Path to the directory to delete
1951
+ * @param {boolean} [recursive=false] - Delete directory and its contents. (only has effect if `move_to_trash` parameter is false.
1952
+ * @param {boolean} [move_to_trash=false] - Move file to trash instead of completely deleting it.
1953
+ * @returns {boolean} - Returns true on success, or throws error on failure
1954
+ * @throws {Error} When the directory path is invalid.
1955
+ * @throws {Error} When the path is not a directory.
1956
+ * @throws {Error} When the directory is not empty and `recursive=false`.
1957
+ * @throws {Error} When failed to delete the directory.
1958
+ */
1959
+ deleteDirectory(dir_path, recursive = false, move_to_trash = false)
1960
+ {
1961
+ if(!(typeof dir_path === "string")) throw new Error(`Invalid "dir_path" argument: ${typeof dir_path}`);
1962
+ if(!this.fileExists(dir_path)) return true;
1963
+ if(!this.fileStat(dir_path).isDirectory) throw new Error(`Failed to delete the directory: Provided path is not a directory. Use ui.deleteFile() instead.`);
1964
+
1965
+ if(move_to_trash)
1966
+ {
1967
+ return this.sendToGUI({system: "moveToTrash", value: dir_path}, r =>
1968
+ {
1969
+ return r.value;
1970
+ });
1971
+ }
1972
+
1973
+ try
1974
+ {
1975
+ // delete the main directory
1976
+ if(this.runtime === "deno")
1977
+ {
1978
+ Deno.removeSync(dir_path, {recursive: recursive === true});
1979
+ }
1980
+ else
1981
+ {
1982
+ if(recursive)
1983
+ {
1984
+ fs.rmSync(dir_path, {recursive: true, force: true});
1985
+ }
1986
+ else
1987
+ {
1988
+ fs.rmdirSync(dir_path);
1989
+ }
1990
+ }
1991
+ return true;
1992
+ }catch(err)
1993
+ {
1994
+ if(this.os === "darwin" && (err.code === "EACCES" || err.code === "ENOTEMPTY"))
1995
+ {
1996
+ try
1997
+ {
1998
+ const script = `do shell script "rm -rf '${dir_path.replace(/'/g, "'\"'\"'")}'" with administrator privileges with prompt "Your permission is needed to delete directory: ${dir_path}"`;
1999
+
2000
+ execFileSync("osascript", ["-e", script]);
2001
+ return true;
2002
+ }catch(err2)
2003
+ {
2004
+ throw err2;
2005
+ }
2006
+ }
2007
+ else
2008
+ {
2009
+ throw err;
2010
+ }
2011
+ }
2012
+ }
2013
+
2014
+ /**
2015
+ * Deletes a file or directory synchronously.
2016
+ *
2017
+ * @param {string} path - Path to the directory to delete
2018
+ * @param {boolean} [recursive=false] - Delete directory and its contents. (only has effect if `move_to_trash` parameter is false.
2019
+ * @param {boolean} [move_to_trash=false] - Move file to trash instead of completely deleting it.
2020
+ * @returns {boolean} - Returns true on success, or throws error on failure
2021
+ * @throws {Error} When the file path is invalid.
2022
+ * @throws {Error} When access is denied.
2023
+ * @throws {Error} When failed to delete the file or directory.
2024
+ */
2025
+ deleteAny(path, recursive = false, move_to_trash = false)
2026
+ {
2027
+ if(!(typeof path === "string")) throw new Error(`Invalid "path" argument: ${typeof path}`);
2028
+ if(!this.fileExists(path)) return true;
2029
+
2030
+ if(this.fileStat(path).isDirectory)
2031
+ {
2032
+ return this.deleteDirectory(path, recursive, move_to_trash);
2033
+ }
2034
+ else
2035
+ {
2036
+ return this.deleteFile(path, move_to_trash);
2037
+ }
2038
+ }
2039
+
2040
+ /**
2041
+ * Synchronously checks if a file can be read.
2042
+ *
2043
+ * @param {string|URL} file_path - Path to the file to check
2044
+ * @returns {boolean} - Returns true if file can be read, false otherwise
2045
+ * @throws {Error} When the file path is invalid.
2046
+ */
2047
+ canReadFile(file_path)
2048
+ {
2049
+ if(!(typeof file_path === "string")) throw new Error(`Invalid "file_path" argument: ${typeof file_path}`);
2050
+
2051
+ try
2052
+ {
2053
+ if(this.runtime === "deno")
2054
+ {
2055
+ const stat = Deno.statSync(file_path);
2056
+ return stat.isFile || stat.isDirectory;
2057
+ }
2058
+ else
2059
+ {
2060
+ fs.accessSync(file_path, fs.constants.R_OK);
2061
+ return true;
2062
+ }
2063
+ }catch(err)
2064
+ {
2065
+ return false;
2066
+ }
2067
+ }
2068
+
2069
+ /**
2070
+ * Synchronously checks if a file can be written to.
2071
+ *
2072
+ * @param {string|URL} file_path - Path to the file to check
2073
+ * @returns {boolean} - Returns true if file can be written to, false otherwise
2074
+ * @throws {Error} When the file path is invalid.
2075
+ */
2076
+ canWriteFile(file_path)
2077
+ {
2078
+ if(!(typeof file_path === "string")) throw new Error(`Invalid "file_path" argument: ${typeof file_path}`);
2079
+ try
2080
+ {
2081
+ if(this.runtime === "deno")
2082
+ {
2083
+ // Check if file exists and is writable
2084
+ const fileInfo = Deno.statSync(file_path);
2085
+ if(!fileInfo.isFile) return false;
2086
+
2087
+ // Try to open with write access
2088
+ const file = Deno.openSync(file_path, {write: true});
2089
+ file.close();
2090
+ return true;
2091
+ }
2092
+ else
2093
+ {
2094
+ fs.accessSync(file_path, fs.constants.W_OK);
2095
+ return true;
2096
+ }
2097
+ }catch(err)
2098
+ {
2099
+ return false;
2100
+ }
2101
+ }
2102
+
2103
+ /**
2104
+ * Creates a directory synchronously.
2105
+ *
2106
+ * @param {string} dir_path - Path to the directory to create
2107
+ * @param {boolean} [recursive=false] - Create parent directories if they don't exist
2108
+ * @returns {boolean} - Returns `true` on success or throws error on failure
2109
+ * @throws {Error} When the directory path is invalid.
2110
+ * @throws {Error} When the directory already exists.
2111
+ * @throws {Error} When failed to create the directory.
2112
+ */
2113
+ makeDirectory(dir_path, recursive = false)
2114
+ {
2115
+ if(!(typeof dir_path === "string")) throw new Error(`Invalid "dir_path" argument: ${typeof dir_path}`);
2116
+
2117
+ if(this.fileExists(dir_path))
2118
+ {
2119
+ if(this.fileStat(dir_path).isDirectory) return true;
2120
+ throw new Error(`Failed to create directory: Path already exists as a file: ${dir_path}`);
2121
+ }
2122
+
2123
+ try
2124
+ {
2125
+ if(this.runtime === "deno")
2126
+ {
2127
+ Deno.mkdirSync(dir_path, {recursive: recursive === true});
2128
+ }
2129
+ else
2130
+ {
2131
+ fs.mkdirSync(dir_path, {recursive: recursive === true});
2132
+ }
2133
+ return true;
2134
+ }catch(err)
2135
+ {
2136
+ throw err;
2137
+ }
2138
+ }
2139
+
2140
+ /**
2141
+ * Moves or copies a file synchronously. If the destination directory doesn't exist, the operation may fail.
2142
+ *
2143
+ * @param {string} from_file_path - Source file path
2144
+ * @param {string} to_file_path - Destination file path
2145
+ * @param {boolean} [copy=false] - Copy instead of move
2146
+ * @returns {boolean} - Returns true on success, or throws an error on failure
2147
+ * @throws {Error} On failure to move or copy the file.
2148
+ */
2149
+ moveFile(from_file_path, to_file_path, copy = false)
2150
+ {
2151
+ if(!(typeof from_file_path === "string")) throw new Error(`Invalid "from_file_path" argument: ${typeof from_file_path}`);
2152
+ if(!(typeof to_file_path === "string")) throw new Error(`Invalid "to_file_path" argument: ${typeof to_file_path}`);
2153
+ if(!this.fileExists(from_file_path)) throw new Error(`Source file not found: ${from_file_path}`);
2154
+ if(!this.fileStat(from_file_path).isFile) throw new Error(`Source path is not a file: ${from_file_path}`);
2155
+
2156
+ try
2157
+ {
2158
+ if(this.runtime === "deno")
2159
+ {
2160
+ if(copy)
2161
+ {
2162
+ Deno.copyFileSync(from_file_path, to_file_path);
2163
+ }
2164
+ else
2165
+ {
2166
+ Deno.renameSync(from_file_path, to_file_path);
2167
+ }
2168
+ }
2169
+ else
2170
+ {
2171
+ if(copy)
2172
+ {
2173
+ fs.copyFileSync(from_file_path, to_file_path);
2174
+ }
2175
+ else
2176
+ {
2177
+ fs.renameSync(from_file_path, to_file_path);
2178
+ }
2179
+ }
2180
+ return true;
2181
+ }catch(err)
2182
+ {
2183
+ throw err;
2184
+ }
2185
+ }
2186
+
2187
+ /**
2188
+ * Moves or copies a directory synchronously. If the destination parent directory doesn't exist, the operation may fail.
2189
+ *
2190
+ * @param {string} from_dir_path - Source directory path
2191
+ * @param {string} to_dir_path - Destination directory path
2192
+ * @param {boolean} [copy=false] - Copy instead of move
2193
+ * @returns {boolean} - Returns true on success, or throws an error on failure
2194
+ * @throws {Error} On failure to move or copy the file.
2195
+ */
2196
+ moveDirectory(from_dir_path, to_dir_path, copy = false)
2197
+ {
2198
+ if(!(typeof from_dir_path === "string")) throw new Error(`Invalid "from_dir_path" argument: ${typeof from_dir_path}`);
2199
+ if(!(typeof to_dir_path === "string")) throw new Error(`Invalid "to_dir_path" argument: ${typeof to_dir_path}`);
2200
+ if(!this.fileExists(from_dir_path)) throw new Error(`Source directory not found: ${from_dir_path}`);
2201
+ if(!this.fileStat(from_dir_path).isDirectory) throw new Error(`Source path is not a directory: ${from_dir_path}`);
2202
+
2203
+ try
2204
+ {
2205
+ if(this.runtime === "deno")
2206
+ {
2207
+ if(copy)
2208
+ {
2209
+ Deno.copySync(from_dir_path, to_dir_path);
2210
+ }
2211
+ else
2212
+ {
2213
+ Deno.renameSync(from_dir_path, to_dir_path);
2214
+ }
2215
+ }
2216
+ else
2217
+ {
2218
+ if(copy)
2219
+ {
2220
+ fs.cpSync(from_dir_path, to_dir_path, {recursive: true});
2221
+ }
2222
+ else
2223
+ {
2224
+ fs.renameSync(from_dir_path, to_dir_path);
2225
+ }
2226
+ }
2227
+ return true;
2228
+ }catch(err)
2229
+ {
2230
+ throw err;
2231
+ }
2232
+ }
2233
+
2234
+ /**
2235
+ * Reads the contents of a directory synchronously and returns an Array of files and directories.
2236
+ *
2237
+ * @param {string} dir_path - Path to the directory to read
2238
+ * @returns {Array<string>} - Returns an Array of filenames/directory names in the directory.
2239
+ * @throws {Error} When the directory path is invalid.
2240
+ * @throws {Error} When the directory is not found.
2241
+ * @throws {Error} When specified path is not a directory.
2242
+ * @throws {Error} When access is denied.
2243
+ * @throws {Error} When failed to read the directory.
2244
+ */
2245
+ listDirectory(dir_path)
2246
+ {
2247
+ if(!(typeof dir_path === "string")) throw new Error(`Invalid argument: ${typeof dir_path}`);
2248
+ if(!this.fileExists(dir_path)) throw new Error(`Directory not found: ${dir_path}`);
2249
+ if(!this.fileStat(dir_path).isDirectory) throw new Error(`Path is not a directory: ${dir_path}`);
2250
+ if(!this.canReadFile(dir_path)) throw new Error(`Failed to read directory, access is denied: ${dir_path}`);
2251
+
2252
+ try
2253
+ {
2254
+ if(this.runtime === "deno")
2255
+ {
2256
+ const entries = [];
2257
+ for(const entry of Deno.readDirSync(dir_path))
2258
+ {
2259
+ entries.push(entry.name);
2260
+ }
2261
+ return entries;
2262
+ }
2263
+ else
2264
+ {
2265
+ return fs.readdirSync(dir_path);
2266
+ }
2267
+ }catch(err)
2268
+ {
2269
+ throw new Error(`Failed to read directory: ${dir_path}`);
2270
+ }
2271
+ }
2272
+
2273
+ /**
2274
+ * Sets or gets a current working directory.
2275
+ *
2276
+ * @param {string} [dir=undefined] - New directory (omit when getting a directory).
2277
+ * @returns {string} Returns current working directory. Throws error if the directory cannot be found or the user doesn't have permissions to access the specified directory when getting.
2278
+ * @throws {Error} When setting working directory failed because it doesn't exist.
2279
+ */
2280
+ cwd(dir = undefined)
2281
+ {
2282
+ if(dir != null && !this.fileExists(dir)) throw new Error(`Directory is not found: ${dir}`);
2283
+
2284
+ if(this.runtime === "deno")
2285
+ {
2286
+ if(dir != null) Deno.chdir(dir);
2287
+ return Deno.cwd();
2288
+ }
2289
+ else
2290
+ {
2291
+ if(dir != null) process.chdir(dir);
2292
+ return process.cwd();
2293
+ }
2294
+ }
2295
+
2296
+ //endregion
2297
+
2298
+ //region Events
2299
+
2300
+ /**
2301
+ * Registering and removing global UI events.
2302
+ *
2303
+ * @internal
2304
+ * @param {string} name
2305
+ * @param {function(any):any|null|undefined} [callback]
2306
+ * @param {boolean} [remove]
2307
+ * @return {this|function[]}
2308
+ */
2309
+ #globalEvent(name, callback = undefined, remove = false)
2310
+ {
2311
+ // return callbacks
2312
+ if(callback === undefined) return Array.isArray(this.callback[name]) ? [...this.callback[name]] : [];
2313
+
2314
+ // remove callback
2315
+ if(remove === true || callback === null)
2316
+ {
2317
+ if(!Array.isArray(this.callback[name])) return this;
2318
+
2319
+ if(callback === null)
2320
+ {
2321
+ this.callback[name].length = 0;
2322
+ }
2323
+ else
2324
+ {
2325
+ const i = this.callback[name].indexOf(callback); // find callback
2326
+ if(i === -1) return this;
2327
+ this.callback[name].splice(i, 1); // remove it
2328
+ }
2329
+
2330
+ if(this.callback[name] != null && this.callback[name].length === 0)
2331
+ {
2332
+ delete this.callback[name]; // delete array
2333
+ this.sendToGUI({java: "globalEvent", remove: name}); // notify GUI
2334
+ }
2335
+ }
2336
+ // add callback
2337
+ else
2338
+ {
2339
+ if(typeof callback !== "function") throw new Error(`Invalid argument for callback function. Must be a "function" type.`);
2340
+
2341
+ if(!Array.isArray(this.callback[name]))
2342
+ {
2343
+ this.callback[name] = []; // create array
2344
+ this.sendToGUI({java: "globalEvent", add: name}); // notify GUI
2345
+ }
2346
+
2347
+ this.callback[name].push(callback);
2348
+ }
2349
+
2350
+ return this;
2351
+ }
2352
+
2353
+ /**
2354
+ * Registers java-wide {@link MouseEvent} event handler that works in all Java apps (but not outside of Java applications).
2355
+ *
2356
+ * @param {function(MouseEvent):any} [callback=undefined] - Function to handle the event
2357
+ * @param {boolean} [remove=false] - Remove the callback from this event
2358
+ * @return {SwingUI|function[]} When called with arguments, returns `this` instance of SwingUI class, otherwise returns an array of current callbacks.
2359
+ * @see MouseEvent
2360
+ */
2361
+ onMouseEvents(callback = undefined, remove = false)
2362
+ {
2363
+ return this.#globalEvent("onMouseEvents", callback, remove === true);
2364
+ }
2365
+
2366
+ /**
2367
+ * Registers java-wide {@link MouseMotionEvent} event handler that works in all Java apps (but not outside of Java applications).
2368
+ *
2369
+ * @param {function(MouseMotionEvent):any} [callback=undefined] - Function to handle the event
2370
+ * @param {boolean} [remove=false] - Remove the callback from this event
2371
+ * @return {SwingUI|function[]} When called with arguments, returns `this` instance of SwingUI class, otherwise returns an array of current callbacks.
2372
+ * @see MouseMotionEvent
2373
+ */
2374
+ onMouseMotion(callback = undefined, remove = false)
2375
+ {
2376
+ return this.#globalEvent("onMouseMotion", callback, remove === true);
2377
+ }
2378
+
2379
+ /**
2380
+ * Registers java-wide {@link MouseWheelEvent} handler that works in all Java apps (but not outside of Java applications).
2381
+ *
2382
+ * @param {function(Object):any} [callback=undefined] - Function to handle the {@link MouseWheelEvent}
2383
+ * @param {boolean} [remove=false] - Remove the callback from this event
2384
+ * @return {SwingUI|function[]} When called with arguments, returns `this` instance of SwingUI class, otherwise returns an array of current callbacks.
2385
+ * @see MouseWheelEvent
2386
+ * @see UIComponent#onMouseWheel
2387
+ */
2388
+ onMouseWheel(callback = undefined, remove = false)
2389
+ {
2390
+ return this.#globalEvent("onMouseWheel", callback, remove === true);
2391
+ }
2392
+
2393
+ /**
2394
+ * Registers java-wide {@link KeyEvent} keyboard key event handler. Handles keyboard keys actions made anywhere on the operating system.
2395
+ *
2396
+ * @param {function(KeyEvent):any} [callback=undefined] - Function to handle the event
2397
+ * @param {boolean} [remove=false] - Remove the callback from this event
2398
+ * @returns {SwingUI|function[]} When called with arguments, returns `this` instance of SwingUI class, otherwise returns an array of current callbacks.
2399
+ * @see KeyEvent
2400
+ */
2401
+ onKey(callback = undefined, remove = false)
2402
+ {
2403
+ return this.#globalEvent("onKey", callback, remove === true);
2404
+ }
2405
+
2406
+ /**
2407
+ * Registers, gets, or unregisters event handler callbacks for application events on macOS.
2408
+ *
2409
+ * @param {function(AppEvent):any} [callback=undefined] - A callback function that receives an {@link AppEvent} object. Set to `null` to unregister all callbacks for this event. Omit to get an array of callbacks for this event.
2410
+ * @param {boolean} [remove=false] - Remove the callback from this event.
2411
+ * @returns {SwingUI|function[]} If called without parameters, an array of registered callbacks is returned; otherwise `this` instance of SwingUI class is returned.
2412
+ * @see AppEvent
2413
+ */
2414
+ onAppEvents(callback = undefined, remove = false)
2415
+ {
2416
+ const name = "onAppEvents";
2417
+
2418
+ // return callbacks
2419
+ if(callback === undefined) return Array.isArray(this.callback[name]) ? [...this.callback[name]] : [];
2420
+
2421
+ // remove callback
2422
+ if(remove === true || callback === null)
2423
+ {
2424
+ if(!Array.isArray(this.callback[name])) return this;
2425
+
2426
+ if(callback === null)
2427
+ {
2428
+ this.callback[name].length = 0;
2429
+ }
2430
+ else
2431
+ {
2432
+ this.callback[name] = this.callback[name].filter(l => l !== callback);
2433
+ }
2434
+
2435
+ if(this.callback[name].length === 0)
2436
+ {
2437
+ delete this.callback[name]; // delete array
2438
+ this.sendToGUI({system: name, remove: true}); // notify GUI
2439
+ }
2440
+ }
2441
+ // add callback
2442
+ else
2443
+ {
2444
+ if(typeof callback !== "function") throw new Error(`Invalid argument for callback function. Must be a "function" type.`);
2445
+
2446
+ if(!Array.isArray(this.callback[name]))
2447
+ {
2448
+ this.callback[name] = []; // create array
2449
+ this.sendToGUI({system: name, remove: false}); // notify GUI
2450
+ }
2451
+
2452
+ this.callback[name].push(callback); // add callback (don't bind to this here, do it at call)
2453
+ }
2454
+
2455
+ return this;
2456
+ }
2457
+
2458
+ /**
2459
+ * Registers, unregisters and retrieves event handler that receive STDIN data sent by command line interface (CLI).
2460
+ *
2461
+ * @param {function(Object):any|null|undefined} [callback] - Function to handle the event, `null` to remove all handlers, or omit to retrieve an array of handlers.
2462
+ * @param {boolean} remove - Removed the callback from this event
2463
+ * @return {SwingUI|function[]} If called without parameters, an array of registered callbacks is returned; otherwise `this` instance of SwingUI class is returned.
2464
+ * @example <h2>Registering an event handler to receive CLI input:</h2>
2465
+ * ui.onCLI(e => {console.log(`Received: ${e.data}`);});
2466
+ */
2467
+ onCLI(callback = undefined, remove = false)
2468
+ {
2469
+ const name = "onCLI";
2470
+
2471
+ // return callbacks
2472
+ if(callback === undefined) return Array.isArray(this.callback[name]) ? [...this.callback[name]] : [];
2473
+
2474
+ // remove callback
2475
+ if(remove === true || callback === null)
2476
+ {
2477
+ if(!Array.isArray(this.callback[name])) return this;
2478
+
2479
+ if(callback === null)
2480
+ {
2481
+ this.callback[name].length = 0;
2482
+ }
2483
+ else
2484
+ {
2485
+ const i = this.callback[name].indexOf(callback); // find callback
2486
+ if(i === -1) return this;
2487
+ this.callback[name].splice(i, 1); // remove it
2488
+ }
2489
+
2490
+ if(this.callback[name].length === 0) delete this.callback[name]; // delete array
2491
+ }
2492
+ else // add callback
2493
+ {
2494
+ if(typeof callback !== "function") throw new Error(`Invalid argument for callback function. Must be a "function" type.`);
2495
+
2496
+ if(!Array.isArray(this.callback[name])) this.callback[name] = []; // create array
2497
+
2498
+ this.callback[name].push(callback); // add callback (don't bind to this here, do it at call)
2499
+ }
2500
+
2501
+ return this;
2502
+ }
2503
+
2504
+ /**
2505
+ * Registers, unregisters and retrieves event handler for an event emitted when UI look and feel is changed using ui.ui(id) method.
2506
+ *
2507
+ * @param {function(Object):any|null|undefined} [callback] - Function to handle the event, `null` to remove all handlers, or omit to retrieve an array of handlers.
2508
+ * @param {boolean} remove - Removed the callback from this event
2509
+ * @return {SwingUI|function[]} If called without parameters, an array of registered callbacks is returned; otherwise `this` instance of SwingUI class is returned.
2510
+ * @example <h2>Receive UI look and feel change events:</h2>
2511
+ * ui.onUIChange(e => {console.log(e);});
2512
+ *
2513
+ * // when UI changes, it prints the event like this:
2514
+ * {
2515
+ * newValue: {
2516
+ * name: "Metal"
2517
+ * dark: false
2518
+ * id: "MetalLookAndFeel"
2519
+ * }
2520
+ * oldValue: {
2521
+ * name: "FlatLaf Dark"
2522
+ * dark: true
2523
+ * id: "FlatDarkLaf"
2524
+ * }
2525
+ * event: "onUIChange"
2526
+ * type: "change"
2527
+ * }
2528
+ * @see SwingUI#list
2529
+ * @see SwingUI#ui
2530
+ */
2531
+ onUIChange(callback = undefined, remove = false)
2532
+ {
2533
+ const name = "onUIChange";
2534
+
2535
+ // return callbacks
2536
+ if(callback === undefined) return Array.isArray(this.callback[name]) ? [...this.callback[name]] : [];
2537
+
2538
+ // remove callback
2539
+ if(remove === true || callback === null)
2540
+ {
2541
+ if(!Array.isArray(this.callback[name])) return this;
2542
+
2543
+ if(callback === null)
2544
+ {
2545
+ this.callback[name].length = 0;
2546
+ }
2547
+ else
2548
+ {
2549
+ this.callback[name] = this.callback[name].filter(l => l !== callback);
2550
+ }
2551
+
2552
+ if(this.callback[name].length === 0)
2553
+ {
2554
+ delete this.callback[name]; // delete array
2555
+ this.sendToGUI({java: name, remove: true}); // notify GUI
2556
+ }
2557
+ }
2558
+ // add callback
2559
+ else
2560
+ {
2561
+ if(typeof callback !== "function") throw new Error(`Invalid argument for callback function. Must be a "function" type.`);
2562
+
2563
+ if(!Array.isArray(this.callback[name]))
2564
+ {
2565
+ this.callback[name] = []; // create array
2566
+ this.sendToGUI({java: name, remove: false}); // notify GUI
2567
+ }
2568
+
2569
+ this.callback[name].push(callback); // add callback (don't bind to this here, do it at call)
2570
+ }
2571
+
2572
+ return this;
2573
+ }
2574
+
2575
+ /**
2576
+ * Registers, unregisters and retrieves event handler for exit event emitted by ui.exit() method.
2577
+ *
2578
+ * @param {function(Object):any|null|undefined} [callback] - Function to handle the event, `null` to remove all handlers, or omit to retrieve an array of handlers.
2579
+ * @param {boolean} remove - Removed the callback from this event
2580
+ * @return {SwingUI|function[]} If called without parameters, an array of registered callbacks is returned; otherwise `this` instance of SwingUI class is returned.
2581
+ * @example <h2>Registering exit event handler:</h2>
2582
+ * ui.onExit(e => {console.log(`Exiting with code: ${e.code}`);});
2583
+ * @see SwingUI#exit
2584
+ */
2585
+ onExit(callback = undefined, remove = false)
2586
+ {
2587
+ const name = "onExit";
2588
+ if(callback === undefined) return Array.isArray(this.callback[name]) ? [...this.callback[name]] : [];
2589
+ if(callback === null)
2590
+ {
2591
+ delete this.callback[name];
2592
+ }
2593
+ else if(remove === true)
2594
+ {
2595
+ const i = this.callback[name].indexOf(callback); // find callback
2596
+ if(i === -1) return this;
2597
+ this.callback[name].splice(i, 1); // remove it
2598
+ }
2599
+ else
2600
+ {
2601
+ if(typeof callback !== "function") throw new Error(`Invalid argument for callback function. Must be a "function" type.`);
2602
+
2603
+ if(!Array.isArray(this.callback[name])) this.callback[name] = []; // create array
2604
+ this.callback[name].push(callback);
2605
+ }
2606
+
2607
+ return this;
2608
+ }
2609
+
2610
+ //endregion
2611
+ }