scripts/path-controller/controller/context.js
/**
see doc_src/context.md
*/
import * as util from '../util/util.js';
import cconst from '../config/config.js';
let notifier = undefined;
export function setNotifier(cls) {
notifier = cls;
}
export const ContextFlags = {
IS_VIEW : 1
};
class InheritFlag {
constructor(data) {
this.data = data;
}
}
let __idgen = 1;
if (Symbol.ContextID === undefined) {
Symbol.ContextID = Symbol("ContextID");
}
if (Symbol.CachedDef === undefined) {
Symbol.CachedDef = Symbol("CachedDef");
}
const _ret_tmp = [undefined];
export const OverlayClasses = [];
export class ContextOverlay {
constructor(appstate) {
this.ctx = undefined; //owning context
this._state = appstate;
}
get state() {
return this._state;
}
/*
Ugly hack, ui_lasttool.js saves
a DataStruct wrapping the most recently executed ToolOp
in this.state._last_tool.
*/
get last_tool() {
return this.state._last_tool;
}
onRemove(have_new_file=false) {
}
copy() {
return new this.constructor(this._state);
}
validate() {
throw new Error("Implement me!");
}
//base classes override this
static contextDefine() {
throw new Error("implement me!");
return {
name : "",
flag : 0
}
}
//don't override this
static resolveDef() {
if (this.hasOwnProperty(Symbol.CachedDef)) {
return this[Symbol.CachedDef];
}
let def2 = Symbol.CachedDef = {};
let def = this.contextDefine();
if (def === undefined) {
def = {};
}
for (let k in def) {
def2[k] = def[k];
}
if (!("flag") in def) {
def2.flag = Context.inherit(0);
}
let parents = [];
let p = util.getClassParent(this);
while (p && p !== ContextOverlay) {
parents.push(p);
p = util.getClassParent(p);
}
if (def2.flag instanceof InheritFlag) {
let flag = def2.flag.data;
for (let p of parents) {
let def = p.contextDefine();
if (!def.flag) {
continue;
}else if (def.flag instanceof InheritFlag) {
flag |= def.flag.data;
} else {
flag |= def.flag;
//don't go past non-inheritable parents
break;
}
}
def2.flag = flag;
}
return def2;
}
}
export const excludedKeys = new Set(["onRemove", "reset", "toString", "_fix",
"valueOf", "copy", "next", "save", "load", "clear", "hasOwnProperty",
"toLocaleString", "constructor", "propertyIsEnumerable", "isPrototypeOf",
"state", "saveProperty", "loadProperty", "getOwningOverlay", "_props"]);
export class LockedContext {
constructor(ctx) {
this.props = {};
this.state = ctx.state;
this.api = ctx.api;
this.toolstack = ctx.toolstack;
this.load(ctx);
}
toLocked() {
//just return itself
return this;
}
error() {
return this.ctx.error(...arguments);
}
warning() {
return this.ctx.warning(...arguments);
}
message() {
return this.ctx.message(...arguments);
}
progbar() {
return this.ctx.progbar(...arguments);
}
load(ctx) {
//let keys = util.getAllKeys(ctx);
let keys = ctx._props;
function wrapget(name) {
return function(ctx2, data) {
return ctx.loadProperty(ctx2, name, data);
}
}
for (let k of keys) {
let v;
if (k === "state" || k === "toolstack" || k === "api") {
continue;
}
if (typeof k === "string" && (k.endsWith("_save") || k.endsWith("_load"))) {
continue;
}
try {
v = ctx[k];
} catch (error) {
if (cconst.DEBUG.contextSystem) {
console.warn("failed to look up property in context: ", k);
}
continue;
}
let data, getter;
let overlay = ctx.getOwningOverlay(k);
if (overlay === undefined) {
//property must no longer be used?
continue;
}
try {
if (typeof k === "string" && (overlay[k + "_save"] && overlay[k + "_load"])) {
data = overlay[k + "_save"]();
getter = overlay[k + "_load"];
} else {
data = ctx.saveProperty(k);
getter = wrapget(k);
}
} catch (error) {
//util.print_stack(error);
console.warn("Failed to save context property", k);
continue;
}
this.props[k] = {
data : data,
get : getter
};
}
let defineProp = (name) => {
Object.defineProperty(this, name, {
get : function() {
let def = this.props[name];
return def.get(this.ctx, def.data)
}
})
};
for (let k in this.props) {
defineProp(k);
}
this.ctx = ctx;
}
setContext(ctx) {
this.ctx = ctx;
this.state = ctx.state;
this.api = ctx.api;
this.toolstack = ctx.toolstack;
}
}
let next_key = {};
let idgen = 1;
export class Context {
constructor(appstate) {
this.state = appstate;
this._props = new Set();
this._stack = [];
this._inside_map = {};
}
/** chrome's debug console corrupts this._inside_map,
this method fixes it*/
_fix() {
this._inside_map = {};
}
fix() {
this._fix();
}
error(message, timeout=1500) {
let state = this.state;
console.warn(message);
if (state && state.screen) {
return notifier.error(state.screen, message, timeout);
}
}
warning(message, timeout=1500) {
let state = this.state;
console.warn(message);
if (state && state.screen) {
return notifier.warning(state.screen, message, timeout);
}
}
message(msg, timeout=1500) {
let state = this.state;
console.warn(msg);
if (state && state.screen) {
return notifier.message(state.screen, msg, timeout);
}
}
progbar(msg, perc=0.0, timeout=1500, id=msg) {
let state = this.state;
if (state && state.screen) {
//progbarNote(screen, msg, percent, color, timeout) {
return notifier.progbarNote(state.screen, msg, perc, "green", timeout, id);
}
}
validateOverlays() {
let stack = this._stack;
let stack2 = [];
for (let i=0; i<stack.length; i++) {
if (stack[i].validate()) {
stack2.push(stack[i]);
}
}
this._stack = stack2;
}
hasOverlay(cls) {
return this.getOverlay(cls) !== undefined;
}
getOverlay(cls) {
for (let overlay of this._stack) {
if (overlay.constructor === cls) {
return overlay;
}
}
}
clear(have_new_file=false) {
for (let overlay of this._stack) {
overlay.onRemove(have_new_file);
}
this._stack = [];
}
//this is implemented by child classes
//it should load the same default overlays as in constructor
reset(have_new_file=false) {
this.clear(have_new_file);
}
//returns a new context with overriden properties
//unlike pushOverlay, overrides can be a simple object
override(overrides) {
if (overrides.copy === undefined) {
overrides.copy = function() {
return Object.assign({}, this);
}
}
let ctx = this.copy();
ctx.pushOverlay(overrides);
return ctx;
}
copy() {
let ret = new this.constructor(this.state);
for (let item of this._stack) {
ret.pushOverlay(item.copy());
}
return ret;
}
/**
Used by overlay property getters. If returned,
the next overlay in the struct will have its getter used.
Example:
class overlay {
get scene() {
if (some_reason) {
return Context.super();
}
return something_else;
}
}
*/
static super() {
return next_key;
}
/**
*
* saves a property into some kind of non-object-reference form
*
* */
saveProperty(key) {
//console.warn("Missing saveProperty implementation in Context; passing through values...", key)
return this[key];
}
/**
*
* lookup property based on saved data
*
* */
loadProperty(ctx, key, data) {
//console.warn("Missing loadProperty implementation in Context; passing through values...", key)
return data;
}
getOwningOverlay(name, _val_out) {
let inside_map = this._inside_map;
let stack = this._stack;
if (cconst.DEBUG.contextSystem) {
console.log(name, inside_map);
}
for (let i=stack.length-1; i >= 0; i--) {
let overlay = stack[i];
let ret = next_key;
if (overlay[Symbol.ContextID] === undefined) {
throw new Error("context corruption");
}
let ikey = overlay[Symbol.ContextID];
if (cconst.DEBUG.contextSystem) {
console.log(ikey, overlay);
}
//prevent infinite recursion
if (inside_map[ikey]) {
continue;
}
if (overlay.__allKeys.has(name)) {
if (cconst.DEBUG.contextSystem) {
console.log("getting value");
}
//Chrome's console messes this up
inside_map[ikey] = 1;
try {
ret = overlay[name];
} catch (error) {
inside_map[ikey] = 0;
throw error;
}
inside_map[ikey] = 0;
}
if (ret !== next_key) {
if (_val_out !== undefined) {
_val_out[0] = ret;
}
return overlay;
}
}
if (_val_out !== undefined) {
_val_out[0] = undefined;
}
return undefined;
}
ensureProperty(name) {
if (this.hasOwnProperty(name)) {
return;
}
this._props.add(name);
Object.defineProperty(this, name, {
get : function() {
let ret = _ret_tmp;
_ret_tmp[0] = undefined;
this.getOwningOverlay(name, ret);
return ret[0];
}, set : function() {
throw new Error("Cannot set ctx properties")
}
})
}
/**
* Returns a new context that doesn't
* contain any direct object references
* except for .state .datalib and .api, but
* instead uses those three to look up references
* on property access.
* */
toLocked() {
return new LockedContext(this);
}
pushOverlay(overlay) {
if (!overlay.hasOwnProperty(Symbol.ContextID)) {
overlay[Symbol.ContextID] = idgen++;
}
let keys = new Set();
for (let key of util.getAllKeys(overlay)) {
if (!excludedKeys.has(key) && !(typeof key === "string" && key[0] === "_")) {
keys.add(key);
}
}
overlay.ctx = this;
if (overlay.__allKeys === undefined) {
overlay.__allKeys = keys;
}
for (let k of keys) {
let bad = typeof k === "symbol" || excludedKeys.has(k);
bad = bad || (typeof k === "string" && k[0] === "_");
bad = bad || (typeof k === "string" && k.endsWith("_save"));
bad = bad || (typeof k === "string" && k.endsWith("_load"));
if (bad) {
continue;
}
this.ensureProperty(k);
}
if (this._stack.indexOf(overlay) >= 0) {
console.warn("Overlay already added once");
if (this._stack[this._stack.length-1] === overlay) {
console.warn(" Definitely an error, overlay is already at top of stack");
return;
}
}
this._stack.push(overlay);
}
popOverlay(overlay) {
if (overlay !== this._stack[this._stack.length-1]) {
console.warn("Context.popOverlay called in error", overlay);
return;
}
overlay.onRemove();
this._stack.pop();
}
removeOverlay(overlay) {
if (this._stack.indexOf(overlay) < 0) {
console.warn("Context.removeOverlay called in error", overlay);
return;
}
overlay.onRemove();
this._stack.remove(overlay);
}
static inherit(data) {
return new InheritFlag(data);
}
static register(cls) {
if (cls[Symbol.ContextID]) {
console.warn("Tried to register same class twice:", cls);
return;
}
cls[Symbol.ContextID] = __idgen++;
OverlayClasses.push(cls);
}
}
export function test() {
function testInheritance() {
class Test0 extends ContextOverlay {
static contextDefine() {
return {
flag: 1
}
}
}
class Test1 extends Test0 {
static contextDefine() {
return {
flag: 2
}
}
}
class Test2 extends Test1 {
static contextDefine() {
return {
flag: Context.inherit(4)
}
}
}
class Test3 extends Test2 {
static contextDefine() {
return {
flag: Context.inherit(8)
}
}
}
class Test4 extends Test3 {
static contextDefine() {
return {
flag: Context.inherit(16)
}
}
}
return Test4.resolveDef().flag === 30;
}
return testInheritance();
}
if (!test()) {
throw new Error("Context test failed");
}