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.
Files changed (116) hide show
  1. package/LICENSE +5 -4
  2. package/README.md +144 -7
  3. package/SwingUI.js +2611 -0
  4. package/classes/Arc.js +185 -0
  5. package/classes/Button.js +55 -0
  6. package/classes/CheckBox.js +33 -0
  7. package/classes/CheckBoxMenuItem.js +24 -0
  8. package/classes/ComboBox.js +329 -0
  9. package/classes/ComponentContainer.js +828 -0
  10. package/classes/ContextMenu.js +141 -0
  11. package/classes/DocumentEditor.js +168 -0
  12. package/classes/Ellipse.js +76 -0
  13. package/classes/InternalWindow.js +397 -0
  14. package/classes/InternalWindowContainer.js +93 -0
  15. package/classes/Label.js +52 -0
  16. package/classes/Line.js +105 -0
  17. package/classes/List.js +452 -0
  18. package/classes/MediaPlayer.js +504 -0
  19. package/classes/Menu.js +74 -0
  20. package/classes/MenuBar.js +56 -0
  21. package/classes/MenuItem.js +92 -0
  22. package/classes/Panel.js +50 -0
  23. package/classes/PasswordField.js +47 -0
  24. package/classes/Polygon.js +236 -0
  25. package/classes/Polyline.js +240 -0
  26. package/classes/ProgressBar.js +62 -0
  27. package/classes/RadioButton.js +110 -0
  28. package/classes/RadioButtonMenuItem.js +106 -0
  29. package/classes/Rectangle.js +199 -0
  30. package/classes/ScrollPane.js +53 -0
  31. package/classes/ScrollableComponent.js +141 -0
  32. package/classes/Separator.js +24 -0
  33. package/classes/Shape.js +584 -0
  34. package/classes/ShapeContainer.js +231 -0
  35. package/classes/Slider.js +172 -0
  36. package/classes/Spinner.js +172 -0
  37. package/classes/SplitView.js +82 -0
  38. package/classes/Stroke.js +115 -0
  39. package/classes/Table.js +982 -0
  40. package/classes/Tabs.js +228 -0
  41. package/classes/TextArea.js +125 -0
  42. package/classes/TextComponent.js +207 -0
  43. package/classes/TextField.js +128 -0
  44. package/classes/TextShape.js +123 -0
  45. package/classes/ToggleButton.js +147 -0
  46. package/classes/ToolBar.js +120 -0
  47. package/classes/TrayIcon.js +54 -0
  48. package/classes/Tree.js +569 -0
  49. package/classes/UIComponent.js +2113 -0
  50. package/classes/Window.js +609 -0
  51. package/enums.js +298 -0
  52. package/fonts.conf +32 -0
  53. package/functions.js +254 -0
  54. package/icon.png +0 -0
  55. package/index.js +109 -4
  56. package/jar/gui-11.jar +0 -0
  57. package/jar/lib/flatlaf.jar +0 -0
  58. package/jar/lib/jna.jar +0 -0
  59. package/package.json +58 -33
  60. package/typedefs.js +973 -0
  61. package/types/SwingUI.d.ts +727 -0
  62. package/types/classes/Arc.d.ts +111 -0
  63. package/types/classes/Button.d.ts +38 -0
  64. package/types/classes/CheckBox.d.ts +22 -0
  65. package/types/classes/CheckBoxMenuItem.d.ts +18 -0
  66. package/types/classes/ComboBox.d.ts +172 -0
  67. package/types/classes/ComponentContainer.d.ts +131 -0
  68. package/types/classes/ContextMenu.d.ts +74 -0
  69. package/types/classes/DocumentEditor.d.ts +95 -0
  70. package/types/classes/Ellipse.d.ts +34 -0
  71. package/types/classes/InternalWindow.d.ts +163 -0
  72. package/types/classes/InternalWindowContainer.d.ts +36 -0
  73. package/types/classes/Label.d.ts +33 -0
  74. package/types/classes/Line.d.ts +48 -0
  75. package/types/classes/List.d.ts +223 -0
  76. package/types/classes/MediaPlayer.d.ts +381 -0
  77. package/types/classes/Menu.d.ts +43 -0
  78. package/types/classes/MenuBar.d.ts +26 -0
  79. package/types/classes/MenuItem.d.ts +57 -0
  80. package/types/classes/Panel.d.ts +28 -0
  81. package/types/classes/PasswordField.d.ts +22 -0
  82. package/types/classes/Polygon.d.ts +93 -0
  83. package/types/classes/Polyline.d.ts +96 -0
  84. package/types/classes/ProgressBar.d.ts +36 -0
  85. package/types/classes/RadioButton.d.ts +70 -0
  86. package/types/classes/RadioButtonMenuItem.d.ts +71 -0
  87. package/types/classes/Rectangle.d.ts +82 -0
  88. package/types/classes/ScrollPane.d.ts +23 -0
  89. package/types/classes/ScrollableComponent.d.ts +81 -0
  90. package/types/classes/Separator.d.ts +14 -0
  91. package/types/classes/Shape.d.ts +223 -0
  92. package/types/classes/ShapeContainer.d.ts +92 -0
  93. package/types/classes/Slider.d.ts +107 -0
  94. package/types/classes/Spinner.d.ts +129 -0
  95. package/types/classes/SplitView.d.ts +47 -0
  96. package/types/classes/Stroke.d.ts +99 -0
  97. package/types/classes/Table.d.ts +652 -0
  98. package/types/classes/Tabs.d.ts +118 -0
  99. package/types/classes/TextArea.d.ts +45 -0
  100. package/types/classes/TextComponent.d.ts +136 -0
  101. package/types/classes/TextField.d.ts +94 -0
  102. package/types/classes/TextShape.d.ts +73 -0
  103. package/types/classes/ToggleButton.d.ts +84 -0
  104. package/types/classes/ToolBar.d.ts +82 -0
  105. package/types/classes/TrayIcon.d.ts +43 -0
  106. package/types/classes/Tree.d.ts +354 -0
  107. package/types/classes/UIComponent.d.ts +945 -0
  108. package/types/classes/Window.d.ts +334 -0
  109. package/types/enums.d.ts +437 -0
  110. package/types/functions.d.ts +109 -0
  111. package/types/index.d.ts +188 -0
  112. package/types/typedefs.d.ts +1626 -0
  113. package/ui-process-manager.js +317 -0
  114. package/video-devices-linux.sh +72 -0
  115. package/video-devices-mac.sh +33 -0
  116. package/CHANGES.md +0 -3
@@ -0,0 +1,828 @@
1
+ import path from "node:path";
2
+ import {UIComponent} from "./UIComponent.js";
3
+
4
+ /**
5
+ * ComponentContainer is not a standalone component and should not be instantiated.
6
+ * It provides methods and members for managing child components.
7
+ * ComponentContainer is extended by {@link Window}, {@link InternalWindowContainer}, {@link InternalWindow}, {@link Panel}, {@link ToolBar}, {@link MenuBar}, {@link Menu}, {@link ContextMenu}, {@link Tabs}, {@link SplitView} and a {@link ScrollPane}.
8
+ * @class
9
+ * @global
10
+ */
11
+ export class ComponentContainer extends UIComponent
12
+ {
13
+ #_children = []; // child components of this component
14
+ #descriptorObject = null;
15
+
16
+ static preInitProperties = ["position", "size", "innerSize"];
17
+ static postInitProperties = ["defaultButton", "state", "visible"]; // state must be applied after all children apply anchor property
18
+
19
+ /**
20
+ * ComponentContainer constructor.
21
+ * @internal
22
+ * @ignore
23
+ */
24
+ constructor(...args)
25
+ {
26
+ // try to load UI from .ui.json file before calling super to override Window type if present.
27
+ const classObject = new.target;
28
+ let root = null;
29
+
30
+ // Do it only if our UI class was extended by a custom class (branch, not a leaf)
31
+ if(!args[1] && (args[0] === "Panel" || args[0] === "Frame" || args[0] === "InternalFrame"))
32
+ {
33
+ LOAD_FILE:{
34
+ // find path of the UI class
35
+ const regex = new RegExp(`at new ${classObject.name} \\((.+?):\\d+:\\d+\\)`);
36
+ const match = new Error().stack.match(regex);
37
+ let ui_file = match ? match[1] : null;
38
+
39
+ // if path can't be found in the stack, skip it
40
+ if(ui_file == null) break LOAD_FILE;
41
+
42
+ // skip UI components that are in classes directory
43
+ //if(/node_modules[\\/]+swing-ui[\\/]+classes[\\/]/.test(ui_file)) break LOAD_FILE;
44
+
45
+ ui_file = ui_file.split("?", 2)[0]; // removes everything after "?"
46
+
47
+ if(ui_file.startsWith("file:///")) ui_file = ui_file.replace(ui.os === "windows" ? "file:///" : "file://", "");
48
+
49
+ // convert class file path to UI file path with .ui.json
50
+ ui_file = ui_file.replaceAll("%20", " ").replace(/\.[^/.]+$/, "") + ".ui.json"; // replaces %20 with spaces, removes file extension, adds new extension
51
+
52
+ if(ui.os === "windows" && ui_file.startsWith("/")) ui_file = ui_file.substring(1);
53
+
54
+ // check if the UI file exists
55
+ if(!ui.fileExists(ui_file)) break LOAD_FILE;
56
+
57
+ const ui_dir = path.dirname(ui_file);
58
+
59
+ let data = null;
60
+
61
+ // read .ui.json file
62
+ try
63
+ {
64
+ data = ui.readFile(ui_file);
65
+ }catch(ex)
66
+ {
67
+ throw new Error(`Failed to read UI definition file: ${ui_file}\n\n${ex.message}\n`);
68
+ }
69
+
70
+ try
71
+ {
72
+ root = JSON.parse(data);
73
+ }catch(ex)
74
+ {
75
+ throw new Error(`Failed to parse UI definition JSON file: ${ui_file}\n\n${ex.message}\n`);
76
+ }
77
+
78
+ root.version = Number.parseFloat(root.version ?? "0");
79
+ root.File = ui_file;
80
+ root.Dir = ui_dir;
81
+
82
+ // grab Window type from definition file and override the Window type passed as an argument
83
+ if(root.version > 0)
84
+ {
85
+ if(root.component?.type === "Window" && root.component?.windowType !== undefined)
86
+ {
87
+ if(args[2] == null) args[2] = {};
88
+
89
+ args[2].type = root.component.windowType;
90
+ delete root.component.windowType; // we don't need it anymore
91
+
92
+ args[2].text = root.component.text;
93
+ delete root.component.text; // we don't need it anymore
94
+ }
95
+ }
96
+ else
97
+ {
98
+ if(root.class === "Window" && root.properties?.type !== undefined)
99
+ {
100
+ if(args[2] == null) args[2] = {};
101
+ args[2].type = root.properties.type;
102
+ delete root.properties.type; // we don't need it anymore
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ super(...args);
109
+
110
+ this.#descriptorObject = root;
111
+
112
+ if(this.#descriptorObject == null)
113
+ {
114
+ this.applyComponentProperties = null;
115
+ this.applyUIDefinition = null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Gets an array of child components.
121
+ *
122
+ * A new array of child components is created each time `.children` is accessed, so you may not want to use it directly inside a `for(let i=0; i < component.children.length, i++){}` or `while(...){}` loops. First save it to a variable, then use it in such loop.
123
+ *
124
+ * In loops `for..in`, `for..of` or `component.children.forEach((c, i)=>{...})` it's accessed only once, so you are looping through the same array.
125
+ *
126
+ * @readonly
127
+ * @type {Object[]}
128
+ * @example <h2>Looping through Window's components</h2>
129
+ * // method 1
130
+ * for(child of window.children)
131
+ * {
132
+ * ...
133
+ * }
134
+ *
135
+ * // method 2
136
+ * const _children = window.children;
137
+ * for(let i = 0; i < _children.length; i++)
138
+ * {
139
+ * const child = _children[i];
140
+ * ...
141
+ * }
142
+ *
143
+ * // method 3
144
+ * window.children.each(child=>{
145
+ * ...
146
+ * });
147
+ */
148
+ get children()
149
+ {
150
+ return [...this.#_children];
151
+ }
152
+
153
+ /**
154
+ * Gets the number of components added to this container.
155
+ * @readonly
156
+ * @type {number}
157
+ */
158
+ get childCount()
159
+ {
160
+ return this.#_children.length;
161
+ }
162
+
163
+ //region Setters / Getters
164
+
165
+ /**
166
+ * Returns a child component at specified index, or `null` if index is outside of range.
167
+ *
168
+ * @param {number} index - Component index (z order).
169
+ * @return {UIComponent|null} A child component at specified index, or `null` if there is no child at specified index.
170
+ */
171
+ child(index)
172
+ {
173
+ return this.#_children[index] ?? null;
174
+ }
175
+
176
+ /**
177
+ * Gets or sets the index of a child component within this container. Setting new index changes z-order of the component.
178
+ *
179
+ * @param {Object} child - Child component
180
+ * @param {number} [new_index] - The new index to assign to the child. Moves the component in z-order.
181
+ * @return {this|null|number} If `new_index` is provided, returns `this` component. If the child is not found in this container, returns `null`. Otherwise, returns the child's current z-index.
182
+ */
183
+ childIndex(child, new_index = undefined)
184
+ {
185
+ const i = this.#_children.indexOf(child);
186
+
187
+ if(new_index == null)
188
+ {
189
+ return i;
190
+ }
191
+ else if(i > -1 && Number.isSafeInteger(new_index))
192
+ {
193
+ new_index = parseInt(new_index);
194
+ child.verifyComponent();
195
+ const args = [Math.min(Math.max(0, parseInt(new_index)), this.#_children.length)]; // validate index
196
+
197
+ if(i !== args[0])
198
+ {
199
+ this.#_children.splice(i, 1);
200
+ this.#_children.splice(args[0], 0, child);
201
+
202
+ child.property("z", args); // update in GUI
203
+ }
204
+ }
205
+ return this;
206
+ }
207
+
208
+ /**
209
+ * Adds a child component to this container component.
210
+ *
211
+ * @param {object} component - UI Component that can be a child of this component.
212
+ * @param {number|object} [data_or_index] - Argument to pass some extra data (such as Tab Name to add Panel to Tabs), or index at which to insert the child. By default component is added to the end.
213
+ * @returns {this} Returns an instance of `this` component for chaining.
214
+ * @see UIComponent#addTo
215
+ * @example <h2>Adding a component at specified index 5</h2>
216
+ * panel.addChild(new CheckBox("Enabled checkbox").position(10, 15), 5); // adds to panel and positions at (10,15) coordinates
217
+ *
218
+ * @example <h2>Adding a component at specified index</h2>
219
+ * tabs.addChild(new Panel(), {text: "Registration", enabled: true, tooltip: "Create new account", icon: "images/user.png"});
220
+ */
221
+ addChild(component, ...args)
222
+ {
223
+ // verify component validity
224
+ if(typeof component !== "object" || !(component instanceof UIComponent)) throw Error(`'component' must be derived from UIComponent of swing-ui module.`);
225
+
226
+ this.verifyComponent();
227
+ component.verifyComponent();
228
+
229
+ if(!this.isContainer) throw new Error(`Cannot add ${component.constructor.name} to ${this.constructor.name}. ${this.constructor.name} cannot contain components.`);
230
+ if(component === this) throw new Error(`Cannot add ${component.constructor.name} to itself.`);
231
+ if(this.insideOf(component)) throw new Error(`Cannot add component's ancestor to the component.`);
232
+
233
+ // remove from previous parent
234
+ if(component.parent_id !== null) component.remove();
235
+
236
+ // add to this container component
237
+ component.parent_id = this.id;
238
+
239
+ let msg = {control: component.id, get: "parent", data: {parent: this.id, index: -1}};
240
+
241
+ if(args.length > 0)
242
+ {
243
+ if(typeof args[0] === "number")
244
+ {
245
+ msg.data.index = parseInt(args[0]);
246
+ }
247
+ else if(typeof args[0] === "object")
248
+ {
249
+ Object.assign(msg.data, args[0]); // add data for components like Tabs
250
+ if(msg.data.index != null) msg.data.index = parseInt(msg.data.index);
251
+ }
252
+ }
253
+
254
+ if(msg.data.index < 0)
255
+ {
256
+ delete msg.data.index;
257
+ this.#_children.push(component);
258
+ }
259
+ else
260
+ {
261
+ if(msg.data.index > this.#_children.length) msg.data.index = this.#_children.length;
262
+ this.#_children.splice(msg.data.index, 0, component); // add at specified index
263
+ }
264
+
265
+ const result = ui.sendToGUI(msg);
266
+
267
+ // check if adding was allowed by GUI
268
+ if(result !== true) throw new Error(`${component.constructor.name} can't be added to ${this.constructor.name}`);
269
+
270
+ // reset position & size for Tabs' and ScrollPane's new components
271
+ if(this instanceof Tabs || this instanceof ScrollPane || this instanceof ToolBar)
272
+ {
273
+ component.position(null);
274
+ component.size(null);
275
+ }
276
+ else
277
+ {
278
+ // apply right and bottom locations set earlier
279
+ if("right" in component.prop.location) component.right(component.prop.location.right);
280
+ if("bottom" in component.prop.location) component.bottom(component.prop.location.bottom);
281
+ if("right" in component.prop.size || "bottom" in component.prop.size)
282
+ {
283
+ const containerSize = (this instanceof Window || this instanceof InternalWindow) ? this.innerSize() : this.size();
284
+
285
+ if("right" in component.prop.size)
286
+ {
287
+ const w = containerSize.width - component.x() - component.prop.size.right;
288
+ delete component.prop.size.right;
289
+
290
+ if("bottom" in component.prop.size)
291
+ {
292
+ let h = containerSize.height - component.y() - component.prop.size.bottom;
293
+ delete component.prop.size.bottom;
294
+
295
+ component.size(w, h);
296
+ }
297
+ else
298
+ {
299
+ component.width(w);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ // emit event
305
+ setImmediate(() =>
306
+ {
307
+ if(!(this.destroyed ?? false))
308
+ this.emit("onContainerEvents", {type: "add", child: component});
309
+ });
310
+
311
+ return this;
312
+ }
313
+
314
+ /**
315
+ * Removes component's child by child object or child index.
316
+ *
317
+ * @param {object|number} child_or_index - Child component object or index of the child.
318
+ * @param {boolean} [recursive=false] - Boolean indicates if all descendents of this child should also be removed.
319
+ * @returns {UIComponent|null} The removed child component, or `null` if there is no child at specified index.
320
+ */
321
+ removeChild(child_or_index, recursive = false)
322
+ {
323
+ let child = undefined;
324
+ let i = -1;
325
+
326
+ if(typeof child_or_index === "object")
327
+ {
328
+ if(!child_or_index instanceof UIComponent) throw new Error(`Invalid argument passed to removeChild(). Component object or index integer is expected.`);
329
+ i = this.#_children.indexOf(child_or_index);
330
+ if(i > -1) child = child_or_index; // check if it's a child
331
+ }
332
+ else if(typeof child_or_index === "number")
333
+ {
334
+ i = child_or_index;
335
+ child = this.#_children[i];
336
+ }
337
+
338
+ if(child == null || i === -1) return null;
339
+
340
+ // remove recursively, in reverse order
341
+ if(recursive) for(let c = child.childCount - 1; c >= 0; c--) child.removeChild(c, true);
342
+
343
+ // disassociate from parent
344
+ child.parent_id = null;
345
+
346
+ // remove from children array
347
+ this.#_children.splice(i, 1);
348
+
349
+ // remove this element from its parent in GUI
350
+ ui.sendToGUI({control: child.id, set: "remove"});
351
+
352
+ // emit event (! MUST BE NOW, because a bit later it may be destroyed)
353
+ this.emit("onContainerEvents", {type: "remove", child: child});
354
+
355
+ return child;
356
+ }
357
+
358
+ /**
359
+ * Removes the control from its parent control and from a list of all components. Also prepares this component to be garbage-collected in JavaScript and GUI to free RAM.
360
+ *
361
+ * @param {boolean} [recursive=false] - Set to true to also destroy all of its descendants recursively
362
+ * @returns {void}
363
+ */
364
+ destroy(...args)
365
+ {
366
+ if(this.destroyed) return;
367
+
368
+ const recursive = (args.length > 0 && args[0] === true);
369
+
370
+ // remove all child components, from last to first (IMPORTANT)
371
+ for(let i = this.#_children.length - 1; i >= 0; i--)
372
+ {
373
+ try
374
+ {
375
+ if(recursive)
376
+ {
377
+ this.#_children[i].destroy(true);
378
+ }
379
+ else
380
+ {
381
+ this.removeChild(i);
382
+ }
383
+ }catch(ex)
384
+ {error(ex);}
385
+ }
386
+
387
+ super.destroy(...args);
388
+ }
389
+
390
+ /**
391
+ * Checks if passed component is a descendant of this container component.
392
+ *
393
+ * @param descendant - Child component to check.
394
+ * @returns {boolean} If the descendant component is inside of this component.
395
+ * @throws {TypeError} When passed argument is not an instance of UIComponent.
396
+ * @see UIComponent#insideOf
397
+ */
398
+ contains(descendant)
399
+ {
400
+ if(!(descendant instanceof UIComponent)) throw new TypeError(`Argument type is invalid. Only UI component is expected.`)
401
+ let parent = descendant.parent;
402
+
403
+ while(parent)
404
+ {
405
+ if(parent === this) return true;
406
+ parent = parent.parent;
407
+ }
408
+
409
+ return false;
410
+ }
411
+ //endregion
412
+
413
+ //region Events
414
+
415
+ /**
416
+ * Gets, registers or unregisters an event handler function for container-related events
417
+ * triggered when a child component is added or removed.
418
+ *
419
+ * @param {function(ContainerEvent): any} [callback=undefined] - The event handler function that receives a {@link ContainerEvent} object. Pass `null` to unregister all handler functions.
420
+ * @param {boolean} [remove=false] - Set to `true` to unregister the event handler.
421
+ * @returns {this|function[]} An array of handler functions if no arguments are passed, otherwise `this` component for method chaining.
422
+ * @see ContainerEvent
423
+ * @example
424
+ * // Example event object:
425
+ * const e = {
426
+ * target: object, // The container component that triggered the event.
427
+ * type: string, // `add`, `remove`.
428
+ * child: object // The child component that was added or removed
429
+ * };
430
+ */
431
+ onContainerEvents(...args)
432
+ {
433
+ if(args.length === 0) return this.event("onContainerEvents");
434
+ if(!this.isContainer) throw new Error(`Calling onContainerEvents() on a non-container component is not allowed.`);
435
+ return this.event("onContainerEvents", args[0], args[1] ?? false);
436
+ }
437
+
438
+ //endregion
439
+
440
+ //region UI designer file Processing
441
+
442
+ /**
443
+ * Loads UI Components and properties from UI designer file FILE_NAME.ui.json where FILE_NAME is the name of JavaScript file of the component.
444
+ *
445
+ * @param {string} class_name - Class name for which to load UI
446
+ * @private
447
+ * @ignore
448
+ */
449
+ applyUIDefinition(class_name)
450
+ {
451
+ // Apply UI to this component
452
+ if(this.#descriptorObject != null)
453
+ {
454
+ const common = {
455
+ ui_file: this.#descriptorObject.File,
456
+ ui_dir: this.#descriptorObject.Dir,
457
+ radio_grouping: [],
458
+ anchors: {},
459
+ isRelativePath: p => (typeof p === "string" && p.length > 0 && !p.startsWith("data:") && !p.startsWith("http:") && !p.startsWith("https:") && !p.startsWith("ftp:") && !path.isAbsolute(p)),
460
+ validateComponent: c =>
461
+ {
462
+ if(!("type" in c)) throw new TypeError(`Component is missing "type" and will not be loaded.`);
463
+ if(typeof globalThis[c.type] !== "function") throw new TypeError(`Component has invalid type '${c.type}' and will not be loaded.`);
464
+ if(!("ref" in c)) throw new TypeError(`Component '${c.type}' is missing "ref" reference property and will not be loaded.`);
465
+ if(c.ref in this) throw new Error(`Component '${c.type}' has 'ref' reference '${c.ref}' matches another component's reference or other property and will not be loaded. Duplicate name detected.`);
466
+ }
467
+ };
468
+
469
+ // Parse UI Designer file
470
+ this.update(false);
471
+ if(this.#descriptorObject.version === 1.0)
472
+ {
473
+ try
474
+ {
475
+ common.validateComponent(this.#descriptorObject.component);
476
+ }catch(ex)
477
+ {
478
+ error(`${ex.name}: ${ex.message}`, false);
479
+ }
480
+
481
+ this.applyComponentPropertiesV1_0(this, this.#descriptorObject.component, common);
482
+ }
483
+ else
484
+ {
485
+ error(`UI descriptor file version is not supported:\n\n${this.#descriptorObject.File}`, false);
486
+ }
487
+ this.#descriptorObject = null;
488
+ this.update(true);
489
+
490
+ // apply grouping to radio components
491
+ for(const _el of common.radio_grouping)
492
+ {
493
+ _el[1] = this[_el[1]];
494
+ // verify that components are from same class
495
+ if(_el[1] instanceof _el[0].constructor) _el[0].group(_el[1]);
496
+ }
497
+
498
+ // apply anchors after all components are added and sized
499
+ Object.entries(common.anchors).forEach(([ref, value]) => {
500
+ this[ref].anchor(value);
501
+ });
502
+ }
503
+
504
+ // delete these functions to make sure they can't be called again
505
+ this.applyComponentProperties = null;
506
+ this.applyComponentPropertiesV1_0 = null;
507
+ this.applyUIDefinition = null;
508
+ }
509
+
510
+ /**
511
+ * @private
512
+ */
513
+ applyComponentPropertiesV1_0(component, properties, common)
514
+ {
515
+ const ref = properties.ref;
516
+ delete properties.ref;
517
+
518
+ const class_name = properties.type;
519
+ delete properties.type;
520
+
521
+ const children = properties.children ?? [];
522
+ delete properties.children;
523
+
524
+ const events = properties.events ?? [];
525
+ delete properties.events;
526
+
527
+ // append directory to icon path if icon path isn't absolute
528
+ if(common.isRelativePath(properties.icon)) properties.icon = path.join(common.ui_dir, properties.icon);
529
+
530
+ // make icon paths absolute
531
+ if(component instanceof List || component instanceof ComboBox || component instanceof Tree)
532
+ {
533
+ if("items" in properties && Array.isArray(properties.items))
534
+ {
535
+ // recursive-substitute method to change icon paths to absolute
536
+ let _stack = [...properties.items];
537
+ while(_stack.length > 0)
538
+ {
539
+ const item = _stack.pop();
540
+ if(!(typeof item === "object") || item === null) continue;
541
+ if(common.isRelativePath(item.icon)) item.icon = path.join(common.ui_dir, item.icon);
542
+ if("children" in item && Array.isArray(item.children)) _stack.push(...item.children);
543
+ }
544
+ }
545
+
546
+ if(component instanceof Tree && "icons" in properties) // tree custom icons
547
+ {
548
+ if(properties.icons != null)
549
+ {
550
+ for(const key of Object.keys(properties.icons))
551
+ {
552
+ if(common.isRelativePath(properties.icons[key])) properties.icons[key] = path.join(common.ui_dir, properties.icons[key]);
553
+ }
554
+ }
555
+ }
556
+ }
557
+ else if(component instanceof Table)
558
+ {
559
+ if("columns" in properties && Array.isArray(properties.columns))
560
+ {
561
+ const _c = properties.columns;
562
+
563
+ // make sure icons' paths are absolute
564
+ for(let c = 0; c < _c.length; c++)
565
+ {
566
+ if(common.isRelativePath(_c[c].icon)) _c[c].icon = path.join(common.ui_dir, _c[c].icon);
567
+
568
+ // if column type is "icon", make rows' icons' paths absolute in this column
569
+ if(_c[c].type === "icon" && "rows" in properties && Array.isArray(properties.rows))
570
+ {
571
+ for(let r = 0; r < properties.rows.length; r++)
572
+ {
573
+ let _r = properties.rows[c] ?? [];
574
+
575
+ if(common.isRelativePath(_r[c])) _r[c] = path.join(common.ui_dir, _r[c]);
576
+ }
577
+ }
578
+ }
579
+ }
580
+ }
581
+
582
+ // apply properties: call function of each property and pass its value
583
+
584
+ // check if pre-initialization properties are present
585
+ const preInitProps = (component instanceof Window && ComponentContainer.preInitProperties.some(key => key in properties));
586
+ const postInitProps = (component instanceof Window && ComponentContainer.postInitProperties.some(key => key in properties));
587
+
588
+ // apply pre-init props first
589
+ if(preInitProps)
590
+ {
591
+ for(const prop of ComponentContainer.preInitProperties)
592
+ {
593
+ if(!(prop in properties)) continue;
594
+
595
+ if(typeof component[prop] === "function")
596
+ {
597
+ component[prop](properties[prop]);
598
+ }
599
+ else
600
+ {
601
+ console.error(`${class_name}.${prop} is not a valid property for one of your components in your .ui.json file:\n\n${common.ui_file}`);
602
+ }
603
+
604
+ delete properties[prop]; // no longer needed
605
+ }
606
+ }
607
+
608
+ // load context menus
609
+ if("contextMenus" in properties)
610
+ {
611
+ if(Array.isArray(properties.contextMenus))
612
+ {
613
+ properties.contextMenus.forEach(json_child =>
614
+ {
615
+ try
616
+ {
617
+ common.validateComponent(json_child);
618
+ }catch(ex)
619
+ {
620
+ error(`Failed to load a context menu. ${ex.name}: ${ex.message}`, false);
621
+ return;
622
+ }
623
+
624
+ this[json_child.ref] = new globalThis[json_child.type]();
625
+ this.applyComponentPropertiesV1_0(this[json_child.ref], json_child, common);
626
+ });
627
+ }
628
+
629
+ delete properties.contextMenus;
630
+ }
631
+
632
+ if(component instanceof Window)
633
+ {
634
+ // load main menu bar
635
+ if("menuBar" in properties)
636
+ {
637
+ try
638
+ {
639
+ common.validateComponent(properties.menuBar);
640
+ }catch(ex)
641
+ {
642
+ error(`Failed to load a menu bar. ${ex.name}: ${ex.message}`, false);
643
+ }
644
+
645
+ const bar = new globalThis[properties.menuBar.type]();
646
+ this.applyComponentPropertiesV1_0(bar, properties.menuBar, common);
647
+ component.menuBar(bar);
648
+ delete properties.menuBar;
649
+ }
650
+
651
+ // load tray icons
652
+ if("trayIcons" in properties)
653
+ {
654
+ if(Array.isArray(properties.trayIcons))
655
+ {
656
+ properties.trayIcons.forEach(json_child =>
657
+ {
658
+ try
659
+ {
660
+ common.validateComponent(json_child);
661
+ }catch(ex)
662
+ {
663
+ error(`Failed to load a tray icon. ${ex.name}: ${ex.message}`, false);
664
+ return;
665
+ }
666
+
667
+ const trayIcon = new globalThis[json_child.type]();
668
+ component.onDispose(e => {if(!trayIcon.destroyed) trayIcon.destroy();}); // destroy the tray icon when window gets disposed
669
+ this[json_child.ref] = trayIcon;
670
+ this.applyComponentPropertiesV1_0(trayIcon, json_child, common);
671
+ });
672
+ }
673
+
674
+ delete properties.trayIcons;
675
+ }
676
+ }
677
+
678
+ // apply properties
679
+ for(const prop of Object.keys(properties))
680
+ {
681
+ if(postInitProps && ComponentContainer.postInitProperties.includes(prop)) continue; // skip post-init props
682
+
683
+ if(prop === "group")
684
+ {
685
+ common.radio_grouping.push([component, properties[prop]]);
686
+ continue;
687
+ }
688
+
689
+ if(typeof component[prop] !== "function")
690
+ {
691
+ error(`"${prop}" is not valid property for ${class_name} component in .ui.json file:\n\n${common.ui_file}`, false);
692
+ continue;
693
+ }
694
+
695
+ let value = properties[prop];
696
+
697
+ switch(prop)
698
+ {
699
+ case "contextMenu":
700
+ {
701
+ if(this[value] instanceof ContextMenu)
702
+ {
703
+ component[prop](this[value]); // replace name of context menu with actual object
704
+ }
705
+ else
706
+ {
707
+ error(`ContextMenu reference "${value}" in ${class_name} "${ref ?? "this"}" is broken in file: \n\n${common.ui_file}`, false);
708
+ }
709
+ break;
710
+ }
711
+ case "items":
712
+ {
713
+ if(component instanceof Tree)
714
+ {
715
+ component[prop]([0], value);
716
+ }
717
+ else
718
+ {
719
+ component[prop](value);
720
+ }
721
+ break;
722
+ }
723
+ case "anchor":
724
+ {
725
+ common.anchors[ref] = value;
726
+ break;
727
+ }
728
+ default:
729
+ {
730
+ component[prop](value);
731
+ }
732
+ }
733
+ }
734
+
735
+ // add components
736
+ if(Array.isArray(children))
737
+ {
738
+ children.forEach(json_child =>
739
+ {
740
+ try
741
+ {
742
+ common.validateComponent(json_child);
743
+ }catch(ex)
744
+ {
745
+ error(`${ex.name}: ${ex.message}`, false);
746
+ return;
747
+ }
748
+
749
+ const child_ref = json_child.ref;
750
+ this[child_ref] = new globalThis[json_child.type]();
751
+ this.applyComponentPropertiesV1_0(this[child_ref], json_child, common);
752
+ component.addChild(this[child_ref]);
753
+ });
754
+ }
755
+
756
+ // apply post-init props
757
+ if(postInitProps)
758
+ {
759
+ for(const prop of ComponentContainer.postInitProperties)
760
+ {
761
+ if(!(prop in properties)) continue;
762
+
763
+ if(typeof component[prop] !== "function")
764
+ {
765
+ error(`"${prop}" is not valid property for ${class_name} in your ui.json file:\n\n${common.ui_file}`, false);
766
+ continue;
767
+ }
768
+
769
+ switch(prop)
770
+ {
771
+ case "defaultButton":
772
+ {
773
+ if(component[properties[prop]] instanceof Button)
774
+ {
775
+ component[prop](component[properties[prop]]);
776
+ }
777
+ else
778
+ {
779
+ error(`Failed to set a default button for this Window. A Button "${properties[prop]}" is not defined.`, false);
780
+ }
781
+ break;
782
+ }
783
+ default:
784
+ {
785
+ component[prop](properties[prop]);
786
+ }
787
+ }
788
+ }
789
+ }
790
+
791
+ // add events
792
+ for(const event of Object.keys(events))
793
+ {
794
+ if(!(event in component))
795
+ {
796
+ error(`${class_name}.${event} is not valid event function for ${class_name} in your .ui.json file:\n\n${common.ui_file}`, false);
797
+ continue;
798
+ }
799
+
800
+ const func = events[event].trim();
801
+
802
+ if(func.startsWith("javascript:"))
803
+ {
804
+ try
805
+ {
806
+ component[event](new Function('e', func.substring(11).trim()).bind(this));
807
+ }catch(ex)
808
+ {
809
+ component[event]((...args) =>
810
+ {
811
+ alert(`${ex.constructor.name} in ${ref}.${event}() event handler:\n\n ${ex.message}`, `Error`, MessageIcon.Error);
812
+ });
813
+ }
814
+ }
815
+ else
816
+ {
817
+ component[event]((...args) =>
818
+ {
819
+ if(!(func in this)) return alert(`Missing ${event} event handler method "this.${func}(event)". Create ${func}(e){ } function.`, `Missing event handler for this.${ref} component`, MessageIcon.Error);
820
+
821
+ this[func](...args);
822
+ });
823
+ }
824
+ }
825
+ }
826
+
827
+ //endregion
828
+ }