|
|
class Renderer extends EventEmitter {
|
|
|
static #WIDGETS = new Map();
|
|
|
static #VIRTUAL_WIDGETS = new Set();
|
|
|
|
|
|
/**
|
|
|
* Register widget class
|
|
|
* @param {string} name
|
|
|
* @param {typeof Widget} cls
|
|
|
* @param {boolean} virtual
|
|
|
*/
|
|
|
static register(name, cls, virtual = false) {
|
|
|
Renderer.#WIDGETS.set(name, cls);
|
|
|
if (virtual) Renderer.#VIRTUAL_WIDGETS.add(name);
|
|
|
}
|
|
|
|
|
|
/** @type {Device} */
|
|
|
device;
|
|
|
#widgets;
|
|
|
#idMap;
|
|
|
#idMapExt;
|
|
|
#files;
|
|
|
#filesLoaded;
|
|
|
|
|
|
constructor(device) {
|
|
|
super();
|
|
|
this.device = device;
|
|
|
this.#widgets = [];
|
|
|
this.#idMap = new Map();
|
|
|
this.#idMapExt = new Map();
|
|
|
this.#files = [];
|
|
|
this.#filesLoaded = false;
|
|
|
}
|
|
|
|
|
|
update(controls) {
|
|
|
this.close();
|
|
|
this.#widgets.length = 0;
|
|
|
this.#idMap.clear();
|
|
|
this.#idMapExt.clear();
|
|
|
this.#files.length = 0;
|
|
|
this.#filesLoaded = false;
|
|
|
|
|
|
this._makeWidgets(this.#widgets, 'col', controls);
|
|
|
this.#filesLoaded = true;
|
|
|
this.#loadFiles();
|
|
|
}
|
|
|
|
|
|
#updateWWidth(type, data) {
|
|
|
switch (type) {
|
|
|
case 'row':
|
|
|
let sumw = 0;
|
|
|
for (const ctrl of data) {
|
|
|
if (!ctrl.type || Renderer.#VIRTUAL_WIDGETS.has(ctrl.type)) continue;
|
|
|
if (!ctrl.wwidth) ctrl.wwidth = 1;
|
|
|
sumw += ctrl.wwidth;
|
|
|
}
|
|
|
for (const ctrl of data) {
|
|
|
if (!ctrl.type || Renderer.#VIRTUAL_WIDGETS.has(ctrl.type)) continue;
|
|
|
ctrl.wwidth_t = ctrl.wwidth * 100 / sumw;
|
|
|
}
|
|
|
break;
|
|
|
|
|
|
case 'col':
|
|
|
for (const ctrl of data) {
|
|
|
if (!ctrl.type || Renderer.#VIRTUAL_WIDGETS.has(ctrl.type)) continue;
|
|
|
ctrl.wwidth_t = 100;
|
|
|
}
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Generate widgets from layout.
|
|
|
* @param {Widget[]} cont
|
|
|
* @param {'row' | 'col'} type
|
|
|
* @param {object[]} data
|
|
|
*/
|
|
|
_makeWidgets(cont, type, data, isExt = false) {
|
|
|
this.#updateWWidth(type, data);
|
|
|
|
|
|
const idMap = isExt ? this.#idMapExt : this.#idMap;
|
|
|
for (const ctrl of data) {
|
|
|
if (!ctrl.type) continue;
|
|
|
|
|
|
const cls = Renderer.#WIDGETS.get(ctrl.type);
|
|
|
if (cls === undefined) {
|
|
|
console.log('W: Missing widget:', ctrl);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
const obj = new cls(ctrl, this);
|
|
|
idMap.set(obj.id, obj)
|
|
|
cont.push(obj);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async _set(widget, value, ack = true) {
|
|
|
try {
|
|
|
await this.device.set(widget.id, value);
|
|
|
} catch (e) {
|
|
|
console.log(e);
|
|
|
if (ack) widget._handleSetError(e);
|
|
|
}
|
|
|
if (ack) widget._handleAck();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Build HTML tree from widgets.
|
|
|
* @returns {HTMLElement}
|
|
|
*/
|
|
|
build(){
|
|
|
const res = [];
|
|
|
for (const w of this.#widgets) {
|
|
|
const $w = w.build();
|
|
|
if ($w) res.push($w);
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Закрытие рендерера (остановка таймеров). Нужно вызвать перед удалением рендерера.
|
|
|
*/
|
|
|
close() {
|
|
|
for (const w of this.#idMap.values()) {
|
|
|
w.close();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Обработчик пакета update с устройства
|
|
|
* @param {string} id Widget id
|
|
|
* @param {object} data
|
|
|
*/
|
|
|
handleUpdate(id, data) {
|
|
|
const w = this.#idMap.get(id);
|
|
|
if (w) w.update(data);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Register an UI file to load.
|
|
|
* @param {Widget} widget
|
|
|
* @param {string} path
|
|
|
* @param {string} type
|
|
|
* @param {(string) => undefined} callback
|
|
|
*/
|
|
|
_addFile(widget, path, type, callback) {
|
|
|
let has = this.#files.some(f => f.widget.id == widget.id);
|
|
|
if (!has) this.#files.push({
|
|
|
widget, path, type, callback
|
|
|
});
|
|
|
this.#loadFiles();
|
|
|
}
|
|
|
|
|
|
async #loadFiles() {
|
|
|
if (!this.#filesLoaded) return;
|
|
|
|
|
|
while (this.#files.length) {
|
|
|
const file = this.#files.shift();
|
|
|
|
|
|
let res;
|
|
|
try {
|
|
|
res = await this.device.fetch(file.path, file.type, file.widget._handleFileProgress.bind(file.widget));
|
|
|
} catch (e) {
|
|
|
console.log(e);
|
|
|
file.widget._handleFileError(e);
|
|
|
continue;
|
|
|
}
|
|
|
file.widget._handleFileLoaded(res);
|
|
|
file.callback(res);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
_getPlugin(name) {
|
|
|
const widget = this.#idMap.get(name);
|
|
|
if (!widget || !(widget instanceof PluginWidget)) return undefined;
|
|
|
return widget.widgetClass;
|
|
|
}
|
|
|
}
|