swing-ui 0.1.25 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -4
- package/README.md +144 -7
- package/SwingUI.js +2611 -0
- package/classes/Arc.js +185 -0
- package/classes/Button.js +55 -0
- package/classes/CheckBox.js +33 -0
- package/classes/CheckBoxMenuItem.js +24 -0
- package/classes/ComboBox.js +329 -0
- package/classes/ComponentContainer.js +828 -0
- package/classes/ContextMenu.js +141 -0
- package/classes/DocumentEditor.js +168 -0
- package/classes/Ellipse.js +76 -0
- package/classes/InternalWindow.js +397 -0
- package/classes/InternalWindowContainer.js +93 -0
- package/classes/Label.js +52 -0
- package/classes/Line.js +105 -0
- package/classes/List.js +452 -0
- package/classes/MediaPlayer.js +504 -0
- package/classes/Menu.js +74 -0
- package/classes/MenuBar.js +56 -0
- package/classes/MenuItem.js +92 -0
- package/classes/Panel.js +50 -0
- package/classes/PasswordField.js +47 -0
- package/classes/Polygon.js +236 -0
- package/classes/Polyline.js +240 -0
- package/classes/ProgressBar.js +62 -0
- package/classes/RadioButton.js +110 -0
- package/classes/RadioButtonMenuItem.js +106 -0
- package/classes/Rectangle.js +199 -0
- package/classes/ScrollPane.js +53 -0
- package/classes/ScrollableComponent.js +141 -0
- package/classes/Separator.js +24 -0
- package/classes/Shape.js +584 -0
- package/classes/ShapeContainer.js +231 -0
- package/classes/Slider.js +172 -0
- package/classes/Spinner.js +172 -0
- package/classes/SplitView.js +82 -0
- package/classes/Stroke.js +115 -0
- package/classes/Table.js +982 -0
- package/classes/Tabs.js +228 -0
- package/classes/TextArea.js +125 -0
- package/classes/TextComponent.js +207 -0
- package/classes/TextField.js +128 -0
- package/classes/TextShape.js +123 -0
- package/classes/ToggleButton.js +147 -0
- package/classes/ToolBar.js +120 -0
- package/classes/TrayIcon.js +54 -0
- package/classes/Tree.js +569 -0
- package/classes/UIComponent.js +2113 -0
- package/classes/Window.js +609 -0
- package/enums.js +298 -0
- package/fonts.conf +32 -0
- package/functions.js +254 -0
- package/icon.png +0 -0
- package/index.js +109 -4
- package/jar/gui-11.jar +0 -0
- package/jar/lib/flatlaf.jar +0 -0
- package/jar/lib/jna.jar +0 -0
- package/package.json +58 -33
- package/typedefs.js +973 -0
- package/types/SwingUI.d.ts +727 -0
- package/types/classes/Arc.d.ts +111 -0
- package/types/classes/Button.d.ts +38 -0
- package/types/classes/CheckBox.d.ts +22 -0
- package/types/classes/CheckBoxMenuItem.d.ts +18 -0
- package/types/classes/ComboBox.d.ts +172 -0
- package/types/classes/ComponentContainer.d.ts +131 -0
- package/types/classes/ContextMenu.d.ts +74 -0
- package/types/classes/DocumentEditor.d.ts +95 -0
- package/types/classes/Ellipse.d.ts +34 -0
- package/types/classes/InternalWindow.d.ts +163 -0
- package/types/classes/InternalWindowContainer.d.ts +36 -0
- package/types/classes/Label.d.ts +33 -0
- package/types/classes/Line.d.ts +48 -0
- package/types/classes/List.d.ts +223 -0
- package/types/classes/MediaPlayer.d.ts +381 -0
- package/types/classes/Menu.d.ts +43 -0
- package/types/classes/MenuBar.d.ts +26 -0
- package/types/classes/MenuItem.d.ts +57 -0
- package/types/classes/Panel.d.ts +28 -0
- package/types/classes/PasswordField.d.ts +22 -0
- package/types/classes/Polygon.d.ts +93 -0
- package/types/classes/Polyline.d.ts +96 -0
- package/types/classes/ProgressBar.d.ts +36 -0
- package/types/classes/RadioButton.d.ts +70 -0
- package/types/classes/RadioButtonMenuItem.d.ts +71 -0
- package/types/classes/Rectangle.d.ts +82 -0
- package/types/classes/ScrollPane.d.ts +23 -0
- package/types/classes/ScrollableComponent.d.ts +81 -0
- package/types/classes/Separator.d.ts +14 -0
- package/types/classes/Shape.d.ts +223 -0
- package/types/classes/ShapeContainer.d.ts +92 -0
- package/types/classes/Slider.d.ts +107 -0
- package/types/classes/Spinner.d.ts +129 -0
- package/types/classes/SplitView.d.ts +47 -0
- package/types/classes/Stroke.d.ts +99 -0
- package/types/classes/Table.d.ts +652 -0
- package/types/classes/Tabs.d.ts +118 -0
- package/types/classes/TextArea.d.ts +45 -0
- package/types/classes/TextComponent.d.ts +136 -0
- package/types/classes/TextField.d.ts +94 -0
- package/types/classes/TextShape.d.ts +73 -0
- package/types/classes/ToggleButton.d.ts +84 -0
- package/types/classes/ToolBar.d.ts +82 -0
- package/types/classes/TrayIcon.d.ts +43 -0
- package/types/classes/Tree.d.ts +354 -0
- package/types/classes/UIComponent.d.ts +945 -0
- package/types/classes/Window.d.ts +334 -0
- package/types/enums.d.ts +437 -0
- package/types/functions.d.ts +109 -0
- package/types/index.d.ts +188 -0
- package/types/typedefs.d.ts +1626 -0
- package/ui-process-manager.js +317 -0
- package/video-devices-linux.sh +72 -0
- package/video-devices-mac.sh +33 -0
- 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("<", "<").replaceAll(">", ">"), 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
|
+
}
|