import uuidv4 from 'uuid/v4'; import { BoundFlags, Bounds, intersect, sizeBounds, XY, xy, XYFlags, boundsSize, moveBounds, boundsPos, ltrb } from './common'; import { Grid, LayoutCell } from './layout'; import { AreaStyle } from './style'; import { WindowSystem } from './winsystem'; import { WindowWidget } from './window'; import { runInThisContext } from 'vm'; import { EventEmitter } from 'events'; export interface WidgetStringContent { type: 'string'; data: string; } export interface WidgetBinaryContent { type: 'binary'; data: Buffer; } export interface WidgetNullContent { type: 'null'; data: null; } export type WidgetContent = WidgetStringContent | WidgetBinaryContent | WidgetNullContent; export interface WidgetLayout { cellPos: XY; cellSize: XY; sticky: BoundFlags; shrink?: boolean; } export interface WidgetLayoutOptions { grow?: XYFlags; shrink?: XYFlags; weight?: XY; } export interface WidgetOptions { id?: string; name?: string; data?: any; activeSystems?: string[]; components?: { [name: string]: any } } export interface WidgetHandler { listeners?: { [event: string]: HandlerListener }; methods?: { [name: string]: HandlerMethod }; data?: (this: MethodThis) => { [name: string]: any }; } export type HandlerListener = (this: MethodThis, origin: BaseWidget, ...args: any[]) => boolean | void; export type HandlerGlobalListener = (event: string, at: BaseWidget, origin: BaseWidget, ...args: any[]) => boolean | void; export type HandlerMethod = ((this: MethodThis, ...args: any[]) => any); export interface MethodThis { [name: string]: HandlerMethod | any; $widget: BaseWidget; $id: string; $methods: { [name: string]: HandlerMethod }; $listeners: { [name: string]: HandlerListener }; } export interface SystemMethodThis extends MethodThis { $origin: BaseWidget; $components: { [name: string]: any }; } export type HandlingSystemListeners = { [name: string]: ( (this: SystemMethodThis, origin: BaseWidget, ...args: any[]) => void ) }; export class HandlingSystem extends EventEmitter { constructor(public name: string, public defs: HandlingSystemListeners) { super(); } activateFor(widget: BaseWidget): void { widget.activeSystems.add(this.name); } deactivateFor(widget: BaseWidget): void { widget.activeSystems.delete(this.name); } emit(event: string, from: BaseWidget, origin: BaseWidget, ...args: any[]): boolean { if (event in this.defs) { let smThis: SystemMethodThis = { $widget: from, $id: from.id, $methods: from.hMethods, $listeners: from.hListeners, $origin: origin, $components: from.components }; Object.keys(from.hMethods).forEach((k) => { Object.defineProperty(smThis, k, { configurable: true, get: () => from.boundMethods()[k] }); }); Object.keys(from.data).forEach((k) => { Object.defineProperty(smThis, k, { configurable: true, set: (x) => from.data[k] = x, get: () => from.data[k] }); }); from.components.forEach((_, k) => { //this[k] = this.$widget.data[k]; Object.defineProperty(smThis, k, { configurable: true, set: (x) => from.components.set(k, x), get: () => from.components.get(k) }); }); this.defs[event].bind(smThis)(origin, ...args); } return super.emit(event, from, origin, ...args); } } export abstract class BaseWidget extends Grid { public id: string; public _name: string | null; public parent?: BaseWidget; public system?: WindowSystem; public children: Map; public data: any; public hMethods: { [name: string]: HandlerMethod }; public hListeners: { [name: string]: HandlerListener }; public globalListeners: Set = new Set(); public components: Map = new Map(); public activeSystems: Set = new Set(); public methodThis: MethodThis; constructor(parent?: Widget | WindowWidget, data?: any) { super(); this.id = uuidv4(); this._name = null; this.children = new Map(); if (data) this.data = data; this.hMethods = {}; this.hListeners = {}; this.methodThis = { $widget: this, $methods: this.hMethods, $id: this.id, $listeners: this.hListeners }; Object.assign(this.methodThis, this.data); } onAny(listener: HandlerGlobalListener) { this.globalListeners.add(listener); } notOnAny(listener: HandlerGlobalListener) { return this.globalListeners.delete(listener); } depth(): number { if (this.parent) return this.parent.depth() + 1; return 0; } name(name?: string | null) { if (name !== undefined) { this._name = name; this.methodThis.$name = name; } return this._name; } boundMethods() { let bound: { [name: string]: HandlerMethod } = {}; Object.keys(this.hMethods).forEach((k) => { let m = this.hMethods[k].bind(this.methodThis); let f: HandlerMethod = function _handlerBound(origin, ...args) { Object.keys(this.$widget.hMethods).forEach((k) => { Object.defineProperty(this, k, { configurable: true, get: () => this.$widget.boundMethods()[k] }); }); Object.keys(this.$widget.data).forEach((k) => { Object.defineProperty(this, k, { configurable: true, set: (x) => this.$widget.data[k] = x, get: () => this.$widget.data[k] }); }); this.$widget.components.forEach((_, k) => { Object.defineProperty(this, k, { configurable: true, set: (x) => this.$widget.components.set(k, x), get: () => this.$widget.components.get(k) }); }); return m(origin, ...args); } bound[k] = f.bind(this.methodThis); }); return bound; } boundListeners() { let bound: { [name: string]: HandlerListener } = {}; Object.keys(this.hListeners).forEach((k) => { bound[k] = this.bindListener(this.hListeners[k]); }); return bound; } bindListener(list: HandlerListener) { let m = list.bind(this.methodThis); let f: HandlerListener = function(origin, ...args) { Object.keys(this.$widget.hMethods).forEach((k) => { Object.defineProperty(this, k, { configurable: true, get: () => this.$widget.boundMethods()[k] }); }); Object.keys(this.$widget.data).forEach((k) => { Object.defineProperty(this, k, { configurable: true, set: (x) => this.$widget.data[k] = x, get: () => this.$widget.data[k] }); }); this.$widget.components.forEach((_, k) => { Object.defineProperty(this, k, { configurable: true, set: (x) => this.$widget.components.set(k, x), get: () => this.$widget.components.get(k) }); }); let res = m(origin, ...args); } return f.bind(this.methodThis); } register(handler: WidgetHandler) { if (handler.data) Object.assign(this.data, handler.data.bind(this.methodThis)()); Object.assign(this.hMethods, handler.methods || {}); if (handler.listeners) Object.keys(handler.listeners as any).forEach((k) => { let l: HandlerListener = (handler.listeners as any)[k]; if (this.hListeners[k]) { let al = this.hListeners[k].bind(this.methodThis); let bl = this.bindListener(l); l = function __combined(this: MethodThis, origin: BaseWidget, ...args: any[]) { al(origin, ...args); bl(origin, ...args); } } this.hListeners[k] = l; }); } abstract getGlobalBounds(): Bounds; emitUp(evt: string | symbol, origin: BaseWidget, ...args: any[]) { let res = this.emit(evt, origin, ...args); if (this.parent) this.parent.emitUp(evt, origin, ...args); return res; } emitUpFromHere(evt: string | symbol, ...args: any[]) { return this.emitUp(evt, this, ...args); } emitDown(evt: string | symbol, origin: BaseWidget, ...args: any[]) { let res = this.emit(evt, origin, ...args); this.children.forEach((c) => { c.emitDown(evt, origin, ...args); }); return res; } emitDownFromHere(evt: string | symbol, ...args: any[]) { return this.emitDown(evt, this, ...args); } emitOut(evt: string | symbol, origin: BaseWidget, ...args: any[]) { let res = this.emit(evt, origin, ...args); if (this.parent) this.parent.emitUp(evt, origin, ...args); this.children.forEach((c) => { c.emitDown(evt, origin, ...args); }); return res; } emit(evt: string | symbol, ...args: [BaseWidget, ...any[]]) { this.globalListeners.forEach((listener) => { listener(evt.toString(), this, ...args); }); if (this.system) { this.activeSystems.forEach((systemName: string) => { if (this.system!.widgetSystems.has(systemName)) this.system!.widgetSystems.get(systemName)!.emit(evt.toString(), this, ...args); }); } let hListeners = this.boundListeners(); if (hListeners[evt.toString()]) hListeners[evt.toString()].bind(this.methodThis)(this, ...args); return super.emit(evt, ...args); } emitHere(evt: string | symbol, ...args:any[]) { return this.emit(evt, this, ...args); } emitOutFromHere(evt: string | symbol, ...args: any[]) { return this.emitOut(evt, this, ...args); } destroy(reason: string | null = null) { this.emitOutFromHere('destroy', reason); if (this.parent) this.parent.removeChild(this); this.children.forEach((c) => { c.destroy(); }) } addChild(w: Widget) { this.children.set(w.id, w); w.emitUpFromHere('added'); } removeChild(w: BaseWidget) { this.children.delete(w.id); } } export interface WidgetData { minSize?: XY; content: WidgetContent; style: AreaStyle; padding?: Bounds; layout: WidgetLayout; } export class Widget extends BaseWidget { public cell?: LayoutCell; public parent: Widget | WindowWidget; public window: WindowWidget; public layoutBounds: Bounds = { left: 0, top: 0, right: 1, bottom: 1 }; public data: WidgetData = { padding: { left: 0, top: 0, right: 0, bottom: 0, }, content: { type: 'null', data: null }, style: { colors: {}, substyles: {}, font: { family: 'Verdana', size: 12, weight: 500 } }, layout: { cellPos: xy(0, 0), cellSize: xy(1, 1), sticky: { left: false, right: false, top: false, bottom: false } } }; minSizeX() { let m = Math.max(this.gridMinWidth(), 0); //console.log(this.name(), 'w 1', m); if (this.data.minSize) { m = Math.max(m, this.data.minSize.x); /*console.log(this.name(), 'w 2', m);*/ } if (this.data.padding) { m += this.data.padding.left + this.data.padding.right; /*console.log(this.name(), 'w 4', m);*/ } return m; } minSizeY() { let m = Math.max(this.gridMinHeight(), 0); if (this.data.minSize) m = Math.max(m, this.data.minSize.y); if (this.data.padding) m += this.data.padding.top + this.data.padding.bottom; return m; } larger() { if (!this.cell || !this.cell.bounds) return false; let sz = this.minSize(); let bz = boundsSize(this.cell!.bounds); if (sz.x > bz.x) return true; if (sz.y > bz.y) return true; return false; } growCell() { if (!this.cell || !this.cell.bounds) return; let sz = this.minSize(); let bz = boundsSize(this.cell!.bounds); if (sz.x > bz.x) this.cell!.bounds.right = this.cell!.bounds.left + sz.x; if (sz.y > bz.y) this.cell!.bounds.bottom = this.cell!.bounds.top + sz.y; } minSize(): XY { return xy(this.minSizeX(), this.minSizeY()); } serialize(system?: WindowSystem) { return { '$wt': 'widget', '$wi': this.id, '$wn': this.name, '$wp': this.parent.id, '$wv': (system || this.system!).prep(this.data), '$wc': (system || this.system!).prep(this.components.entries()), '$ws': Array.from(this.activeSystems), }; } static deserialize(sys: WindowSystem, data: any) { return Widget.make(sys.widgets.get(data.$wp) as Widget | WindowWidget, { data: sys.unprep(data.$wv), id: data.$wi, name: data.$wn, components: new Map(sys.unprep(data.$wc)), activeSystems: data.$ws }); } inheritStyle(from: AreaStyle = this.parent.inheritStyle()): AreaStyle { let res: AreaStyle = { colors: {}, substyles: { selected: {}, active: {}, hover: {} }, font: { family: 'Verdana', size: 12, weight: 500 } }; if (from) { Object.assign(res.colors, from.colors); if (from.substyles.selected) Object.assign(res.substyles.selected, from.substyles.selected); if (from.substyles.active) Object.assign(res.substyles.active, from.substyles.active); if (from.substyles.hover) Object.assign(res.substyles.hover, from.substyles.hover); } if (this.data.style.substyles.selected) Object.assign(res.substyles.selected, this.data.style.substyles.selected); if (this.data.style.substyles.active) Object.assign(res.substyles.active, this.data.style.substyles.active); if (this.data.style.substyles.hover) Object.assign(res.substyles.hover, this.data.style.substyles.hover); Object.assign(res.colors, this.data.style.colors); return res; } destroy(reason: string | null = null) { super.destroy(reason); if (this.window && this.window.system) { this.window.system.widgets.delete(this.id); } } static make(parent: Widget | WindowWidget, options: WidgetOptions) { let res = new Widget(parent, {}, options.id, options.name); if (options.data) { if (options.data.minSize) res.data.minSize = options.data.minSize; if (options.data.content) res.data.content = options.data.content; if (options.data.padding) res.data.padding = options.data.padding; if (options.data.layout) Object.assign(res.data.layout, options.data.layout); if (options.data.style) Object.assign(res.data.style, options.data.style); } if (options.components) res.components = new Map(Object.entries(options.components)); if (options.activeSystems) res.activeSystems = new Set(options.activeSystems); parent.addAsCell(res); return res; } constructor(parent: Widget | WindowWidget, data: any = {}, id?: string, name?: string) { super(parent, {}); let lt = this.data.layout; Object.assign(lt, data.layout || {}); data.layout = lt; Object.assign(this.data, data); this.parent = parent; if (id) this.id = id; if (name) this._name = name; if (parent instanceof WindowWidget) this.window = parent; else this.window = parent.window; if (parent) { this.parent = parent as Widget | WindowWidget; this.parent.addChild(this); } } initialBounds(): Bounds { let is = this.minSize(); if (!this.cell) return sizeBounds(is); let cb = this.cell.getBounds(); let sizes = boundsSize(cb); let res = { left: cb.left + sizes.x / 2 - is.x / 2, top: cb.top + sizes.y / 2 - is.y / 2, right: cb.left + sizes.x / 2 + is.x / 2, bottom: cb.top + sizes.y / 2 + is.y / 2, } return res; } cellResized() { if (this.cell) { let cb = this.cell.getBounds(); this.layoutBounds = this.initialBounds(); if (this.data.layout.sticky.left) this.layoutBounds.left = Math.min(this.layoutBounds.left, cb.left); if (this.data.layout.sticky.top) this.layoutBounds.top = Math.min(this.layoutBounds.top, cb.top); if (this.data.layout.sticky.right) this.layoutBounds.right = Math.max(this.layoutBounds.right, cb.right); if (this.data.layout.sticky.bottom) this.layoutBounds.bottom = Math.max(this.layoutBounds.bottom, cb.bottom); } } getGlobalBounds(): Bounds { let res = JSON.parse(JSON.stringify(this.layoutBounds)); if (this.parent) { let pb = this.parent.getGlobalBounds(); res = intersect(moveBounds(res, boundsPos(pb)), pb); } return res; } sizeX() { return this.layoutBounds.right - this.layoutBounds.left; } sizeY() { return this.layoutBounds.bottom - this.layoutBounds.top; } size(): XY { return xy(this.sizeX(), this.sizeY()); } padding(): Bounds { if (this.data.padding) { return JSON.parse(JSON.stringify(this.data.padding)); } return super.padding(); } }