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
|
@@ -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
|
+
}
|