scripts/screen/FrameManager.js
import {ToolTipViewer} from "./FrameManager_ops.js";
let _FrameManager = undefined;
import '../widgets/dragbox.js';
import '../widgets/ui_widgets2.js';
import '../widgets/ui_panel.js';
import '../widgets/ui_treeview.js';
import {DataPathError, nstructjs} from '../path-controller/controller.js';
import '../util/ScreenOverdraw.js';
import cconst from '../config/const.js';
import {haveModal, pushModalLight, popModalLight, _setScreenClass} from '../path-controller/util/simple_events.js';
import * as util from '../path-controller/util/util.js';
import '../widgets/ui_curvewidget.js';
import * as vectormath from '../path-controller/util/vectormath.js';
import * as ui_base from '../core/ui_base.js';
import * as ScreenArea from './ScreenArea.js';
import * as FrameManager_ops from './FrameManager_ops.js';
import * as math from '../path-controller/util/math.js';
import * as ui_menu from '../widgets/ui_menu.js';
import '../path-controller/util/struct.js';
import {KeyMap, HotKey} from '../path-controller/util/simple_events.js';
import {keymap} from "../path-controller/util/simple_events.js";
import {AreaDocker} from './AreaDocker.js';
import {snap, snapi, ScreenBorder, ScreenVert, ScreenHalfEdge, SnapLimit} from "./FrameManager_mesh.js";
export {ScreenBorder, ScreenVert, ScreenHalfEdge} from "./FrameManager_mesh.js";
import {theme, PackFlags} from '../core/ui_base.js';
import * as FrameManager_mesh from './FrameManager_mesh.js';
import {makePopupArea} from "../widgets/ui_dialog.js";
let Area = ScreenArea.Area;
import '../widgets/ui_widgets.js';
import '../widgets/ui_tabs.js';
import '../widgets/ui_colorpicker2.js';
import '../widgets/ui_noteframe.js';
import '../widgets/ui_listbox.js';
import '../widgets/ui_table.js';
import {AreaFlags, setScreenClass} from "./ScreenArea.js";
import {checkForTextBox} from '../widgets/ui_textbox.js';
import {startMenu} from '../widgets/ui_menu.js';
function list(iter) {
let ret = [];
for (let item of iter) {
ret.push(item);
}
return ret;
}
ui_menu.startMenuEventWrangling();
let _events_started = false;
export function registerToolStackGetter(func) {
FrameManager_ops.registerToolStackGetter(func);
}
let Vector2 = vectormath.Vector2,
UIBase = ui_base.UIBase,
styleScrollBars = ui_base.styleScrollBars;
let update_stack = new Array(8192);
update_stack.cur = 0;
let screen_idgen = 0;
export function purgeUpdateStack() {
for (let i = 0; i < update_stack.length; i++) {
update_stack[i] = undefined;
}
}
/**
* Base class for app workspaces
*
attributes:
inherit-scale : don't resize to fit whole screen, use cssbox scaling
*/
export class Screen extends ui_base.UIBase {
constructor() {
super();
this.snapLimit = 1;
this.fullScreen = true;
//all widget shadow DOMs reference this style tag,
//or rather they copy it
this.globalCSS = document.createElement("style");
this.shadow.prepend(this.globalCSS);
this._do_updateSize = true;
this._resize_callbacks = [];
this.allBordersMovable = cconst.DEBUG.allBordersMovable;
this.needsBorderRegen = true;
this._popup_safe = 0;
//if true, will test all areas for keymaps on keypress,
//not just the active one
this.testAllKeyMaps = false;
this.needsTabRecalc = true;
this._screen_id = screen_idgen++;
this._popups = [];
this._ctx = undefined;
this.keymap = new KeyMap();
this.size = new Vector2([window.innerWidth, window.innerHeight]);
this.pos = new Vector2();
this.oldpos = new Vector2();
this.idgen = 0;
this.sareas = [];
this.sareas.active = undefined;
this.mpos = [0, 0];
this.screenborders = [];
this.screenverts = [];
this._vertmap = {};
this._edgemap = {};
this._idmap = {};
//effective bounds of screen
this._aabb = [new Vector2(), new Vector2()];
let on_mousemove = (e, x, y) => {
//let elem = this.pickElement(x, y, 1, 1, ScreenArea.ScreenArea);
let dragging = e.type === "mousemove" || e.type === "touchmove" || e.type === "pointermove";
dragging = dragging && (e.buttons || (e.touches && e.touches.length > 0));
/*
make sure active area is up to date.
but don't call pickElement too often as it's slow
*/
if (!dragging && Math.random() > 0.9) {
let elem = this.pickElement(x, y, {
sx : 1,
sy : 1,
nodeclass : ScreenArea.ScreenArea,
mouseEvent: e
});
if (0) {
let elem2 = this.pickElement(x, y, 1, 1);
console.log("" + this.sareas.active, elem2 ? elem2.tagName : undefined, elem !== undefined);
}
if (elem !== undefined) {
if (elem.area) {
//make sure context area stacks are up to date
elem.area.push_ctx_active();
elem.area.pop_ctx_active();
}
this.sareas.active = elem;
}
}
this.mpos[0] = x;
this.mpos[1] = y;
}
this.shadow.addEventListener("mousemove", (e) => {
return on_mousemove(e, e.x, e.y);
}, {passive: true});
/*UIBase forwards touch events already
this.shadow.addEventListener("touchmove", (e) => {
if (e.touches.length === 0) {
return;
}
return on_mousemove(e, e.touches[0].pageX, e.touches[0].pagesY);
}, {passive : true});
*/
}
get borders() {
let this2 = this;
return (function* () {
for (let k in this2._edgemap) {
yield this2._edgemap[k];
}
})();
}
get listening() {
return this.listen_timer !== undefined;
}
get ctx() {
return this._ctx;
}
set ctx(val) {
this._ctx = val;
//fully recurse tree
let rec = (n) => {
if (n instanceof UIBase) {
n.ctx = val;
}
for (let n2 of n.childNodes) {
rec(n2);
}
if (n.shadow) {
for (let n2 of n.shadow.childNodes) {
rec(n2);
}
}
}
for (let n of this.childNodes) {
rec(n);
}
for (let n of this.shadow.childNodes) {
rec(n);
}
}
static fromJSON(obj, schedule_resize = false) {
let ret = UIBase.createElement(this.define().tagname);
return ret.loadJSON(obj, schedule_resize);
}
static define() {
return {
tagname: "pathux-screen-x"
};
}
static newSTRUCT() {
return UIBase.createElement(this.define().tagname);
}
setPosSize(x, y, w, h) {
this.pos[0] = x;
this.pos[1] = y;
this.size[0] = w;
this.size[1] = h;
this.setCSS();
this._internalRegenAll();
}
setSize(w, h) {
this.size[0] = w;
this.size[1] = h;
this.setCSS();
this._internalRegenAll();
}
setPos(x, y) {
this.pos[0] = x;
this.pos[1] = y;
this.setCSS();
this._internalRegenAll();
}
init() {
super.init();
if (this.hasAttribute("listen")) {
this.listen();
}
}
/**
*
* @param {*} style May be a string, a CSSStyleSheet instance, or a style tag
* @returns Promise fulfilled when style has been merged
*/
mergeGlobalCSS(style) {
return new Promise((accept, reject) => {
let sheet;
let finish = () => {
let sheet2 = this.globalCSS.sheet;
if (!sheet2) {
this.doOnce(finish);
return;
}
let map = {};
for (let rule of sheet2.rules) {
map[rule.selectorText] = rule;
}
for (let rule of sheet.rules) {
let k = rule.selectorText;
if (k in map) {
let rule2 = map[k];
if (!rule.styleMap) { //handle firefox
for (let k in rule.style) {
let desc = Object.getOwnPropertyDescriptor(rule.style, k);
if (!desc || !desc.writable) {
continue;
}
let v = rule.style[k];
if (v) {
rule2.style[k] = rule.style[k];
}
}
continue;
}
for (let [key, val] of list(rule.styleMap.entries())) {
if (1 || rule2.styleMap.has(key)) {
//rule2.styleMap.delete(key);
let sval = "";
if (Array.isArray(val)) {
for (let item of val) {
sval += " " + val;
}
sval = sval.trim();
} else {
sval = ("" + val).trim();
}
rule2.style[key] = sval;
rule2.styleMap.set(key, val);
} else {
rule2.styleMap.append(key, val);
}
}
} else {
sheet2.insertRule(rule.cssText);
}
}
}
if (typeof style === "string") {
try { //stupid firefox
sheet = new CSSStyleSheet();
} catch (error) {
sheet = undefined;
}
if (sheet && sheet.replaceSync) {
sheet.replaceSync(style);
finish();
} else {
let tag = document.createElement("style");
tag.textContent = style;
document.body.appendChild(tag);
let cb = () => {
if (!tag.sheet) {
this.doOnce(cb);
return;
}
sheet = tag.sheet;
finish();
tag.remove();
};
this.doOnce(cb)
}
} else if (!(style instanceof CSSStyleSheet)) {
sheet = style.sheet;
finish();
} else {
sheet = style;
finish();
}
});
}
newScreenArea() {
let ret = UIBase.createElement("screenarea-x");
ret.ctx = this.ctx;
if (ret.ctx) {
ret.init();
}
return ret;
}
copy() {
let ret = UIBase.createElement(this.constructor.define().tagname);
ret.ctx = this.ctx;
ret._init();
for (let sarea of this.sareas) {
let sarea2 = sarea.copy(ret);
sarea2._ctx = this.ctx;
sarea2.screen = ret;
sarea2.parentWidget = ret;
ret.appendChild(sarea2);
}
for (let sarea of ret.sareas) {
sarea.ctx = this.ctx;
sarea.area.ctx = this.ctx;
sarea.area.push_ctx_active();
sarea._init();
sarea.area._init();
sarea.area.pop_ctx_active();
for (let area of sarea.editors) {
area.ctx = this.ctx;
area.push_ctx_active();
area._init();
area.pop_ctx_active();
}
}
ret.update();
ret.regenBorders();
ret.setCSS();
return ret;
}
findScreenArea(x, y) {
for (let i = this.sareas.length - 1; i >= 0; i--) {
let sarea = this.sareas[i];
let ok = x >= sarea.pos[0] && x <= sarea.pos[0] + sarea.size[0];
ok = ok && (y >= sarea.pos[1] && y <= sarea.pos[1] + sarea.size[1]);
if (ok) {
return sarea;
}
}
}
/**
* @param x
* @param y
* @param args arguments : {sx, sy, nodeclass, excluded_classes}
*/
pickElement(x, y, args, sy, nodeclass, excluded_classes) {
let sx;
let clip;
if (typeof args === "object") {
sx = args.sx;
sy = args.sy;
nodeclass = args.nodeclass;
excluded_classes = args.excluded_classes;
clip = args.clip;
} else {
sx = args;
args = {
sx : sx,
sy : sy,
nodeclass : nodeclass,
excluded_classes: excluded_classes
};
}
if (!this.ctx) {
console.warn("no ctx in screen");
return;
}
let ret;
for (let i = this._popups.length - 1; i >= 0; i--) {
let popup = this._popups[i];
ret = ret || popup.pickElement(x, y, args);
}
ret = ret || super.pickElement(x, y, args);
return ret;
}
_enterPopupSafe() {
if (this._popup_safe === undefined) {
this._popup_safe = 0;
}
this._popup_safe++;
}
* _allAreas() {
for (let sarea of this.sareas) {
for (let area of sarea.editors) {
yield [area, area._area_id, sarea];
}
}
}
_exitPopupSafe() {
this._popup_safe = Math.max(this._popup_safe - 1, 0);
}
popupMenu(menu, x, y) {
startMenu(menu, x, y);
for (let i = 0; i < 3; i++) {
menu.flushSetCSS();
menu.flushUpdate();
}
return menu;
}
/**
*
* @param popupDelay : if non-zero, wait for popup to layout for popupDelay miliseconds,
* then move the popup so it's fully inside the window (if it's outsize).
*
* */
popup(owning_node, elem_or_x, y, closeOnMouseOut = true, popupDelay = 5) {
let ret = this._popup(...arguments);
for (let i = 0; i < 2; i++) {
ret.flushUpdate();
ret.flushSetCSS();
}
if (popupDelay === 0) {
return ret;
}
let z = ret.style["z-index"];
ret.style["z-index"] = "-10";
let cb = () => {
let rect = ret.getClientRects()[0];
let size = this.size;
if (!rect) {
this.doOnce(cb);
return;
}
//console.log("rect", rect);
if (rect.bottom > size[1]) {
ret.style["top"] = (size[1] - rect.height - 10) + "px";
} else if (rect.top < 0) {
ret.style["top"] = "10px";
}
if (rect.right > size[0]) {
ret.style["left"] = (size[0] - rect.width - 10) + "px";
} else if (rect.left < 0) {
ret.style["left"] = "10px";
}
ret.style["z-index"] = z;
ret.flushUpdate();
ret.flushSetCSS();
}
setTimeout(cb, popupDelay);
//this.doOnce(cb);
return ret;
}
draggablePopup(x, y) {
let ret = UIBase.createElement("drag-box-x");
ret.ctx = this.ctx;
ret.parentWidget = this;
ret._init();
this._popups.push(ret);
ret._onend = () => {
if (this._popups.indexOf(ret) >= 0) {
this._popups.remove(ret);
}
}
ret.style["z-index"] = 205;
ret.style["position"] = UIBase.PositionKey;
ret.style["left"] = x + "px";
ret.style["top"] = y + "px";
document.body.appendChild(ret);
return ret;
}
/** makes a popup at x,y and returns a new container-x for it */
_popup(owning_node, elem_or_x, y, closeOnMouseOut = true) {
let x;
let sarea = this.sareas.active;
let w = owning_node;
while (w) {
if (w instanceof ScreenArea.ScreenArea) {
sarea = w;
break;
}
w = w.parentWidget;
}
if (typeof elem_or_x === "object") {
let r = elem_or_x.getClientRects()[0];
x = r.x;
y = r.y;
} else {
x = elem_or_x;
}
x += window.scrollX;
y += window.scrollY;
let container = UIBase.createElement("container-x");
container.ctx = this.ctx;
container._init();
let remove = container.remove;
container.remove = () => {
if (this._popups.indexOf(container) >= 0) {
this._popups.remove(container);
}
return remove.apply(container, arguments);
};
container.overrideClass("popup");
container.background = container.getDefault("background-color");
container.style["border-radius"] = container.getDefault("border-radius") + "px";
container.style["border-color"] = container.getDefault("border-color");
container.style["border-style"] = container.getDefault("border-style");
container.style["border-width"] = container.getDefault("border-width") + "px";
container.style["box-shadow"] = container.getDefault("box-shadow");
container.style["position"] = UIBase.PositionKey;
container.style["z-index"] = "2205";
container.style["left"] = x + "px";
container.style["top"] = y + "px";
container.style["margin"] = "0px";
container.parentWidget = this;
let mm = new math.MinMax(2);
let p = new Vector2();
let _update = container.update;
container.update.after(() => {
container.style["z-index"] = "2205";
});
/*causes weird bugs
container.update = () => {
_update.call(container);
let rects = container.getClientRects();
mm.reset();
for (let r of rects) {
p[0] = r.x;
p[1] = r.y;
mm.minmax(p);
p[0] += r.width;
p[1] += r.height;
mm.minmax(p);
}
let x = mm.min[0], y = mm.min[1];
x = Math.min(x, this.size[0]-(mm.max[0]-mm.min[0]));
y = Math.min(y, this.size[1]-(mm.max[1]-mm.min[1]));
container.style["left"] = x + "px";
container.style["top"] = y + "px";
}//*/
document.body.appendChild(container);
//this.shadow.appendChild(container);
this.setCSS();
this._popups.push(container);
let touchpick, mousepick, keydown;
let done = false;
let end = () => {
if (this._popup_safe) {
return;
}
if (done) return;
//this.ctx.screen.removeEventListener("touchstart", touchpick, true);
//this.ctx.screen.removeEventListener("touchmove", touchpick, true);
this.ctx.screen.removeEventListener("mousedown", mousepick, true);
this.ctx.screen.removeEventListener("mousemove", mousepick, {passive: true});
this.ctx.screen.removeEventListener("mouseup", mousepick, true);
window.removeEventListener("keydown", keydown);
done = true;
container.remove();
};
container.end = end;
let _remove = container.remove;
container.remove = function () {
if (arguments.length == 0) {
end();
}
_remove.apply(this, arguments);
};
container._ondestroy = () => {
end();
};
let bad_time = util.time_ms();
let last_pick_time = util.time_ms();
mousepick = (e, x, y, do_timeout = true) => {
if (!container.isConnected) {
end();
return;
}
if (sarea && sarea.area) {
sarea.area.push_ctx_active();
sarea.area.pop_ctx_active();
}
//console.log("=======================================================popup touch start");
//console.log(e);
if (util.time_ms() - last_pick_time < 350) {
return;
}
last_pick_time = util.time_ms();
x = x === undefined ? e.x : x;
y = y === undefined ? e.y : y;
//let elem = this.pickElement(x, y, 2, 2, undefined, [ScreenBorder]);
let elem = this.pickElement(x, y, {
sx : 2,
sy : 2,
excluded_classes: [ScreenBorder],
mouseEvent : e
});
let startelem = elem;
if (elem === undefined) {
if (closeOnMouseOut) {
end();
}
return;
}
let ok = false;
let elem2 = elem;
while (elem) {
if (elem === container) {
ok = true;
break;
}
elem = elem.parentWidget;
}
if (!ok) {
do_timeout = !do_timeout || (util.time_ms() - bad_time > 100);
if (closeOnMouseOut && do_timeout) {
end();
}
} else {
bad_time = util.time_ms();
}
};
touchpick = (e) => {
let x = e.touches[0].pageX, y = e.touches[0].pageY;
return mousepick(e, x, y, false);
};
keydown = (e) => {
if (!container.isConnected) {
window.removeEventListener("keydown", keydown);
return;
}
switch (e.keyCode) {
case keymap["Escape"]:
end();
break;
}
};
//this.ctx.screen.addEventListener("touchstart", touchpick, true);
//this.ctx.screen.addEventListener("touchmove", touchpick, true);
this.ctx.screen.addEventListener("mousedown", mousepick, true);
this.ctx.screen.addEventListener("mousemove", mousepick, {passive: true});
this.ctx.screen.addEventListener("mouseup", mousepick, true);
window.addEventListener("keydown", keydown);
/*
container.addEventListener("mouseleave", (e) => {
console.log("popup mouse leave");
if (closeOnMouseOut)
end();
});
container.addEventListener("mouseout", (e) => {
console.log("popup mouse out");
if (closeOnMouseOut)
end();
});
//*/
this.calcTabOrder();
return container;
}
_recalcAABB(save = true) {
let mm = new math.MinMax(2);
for (let v of this.screenverts) {
mm.minmax(v);
}
if (save) {
this._aabb[0].load(mm.min);
this._aabb[1].load(mm.max);
}
return [new Vector2(mm.min), new Vector2(mm.max)];
}
//XXX look at if this is referenced anywhere
load() {
}
//XXX look at if this is referenced anywhere
save() {
}
popupArea(area_class) {
return makePopupArea(area_class, this);
}
remove(trigger_destroy = true) {
this.unlisten();
if (trigger_destroy) {
return super.remove();
} else {
HTMLElement.prototype.remove.call(this);
}
}
unlisten() {
if (this.listen_timer !== undefined) {
window.clearInterval(this.listen_timer);
this.listen_timer = undefined;
}
}
checkCSSSize() {
let w = this.style.width.toLowerCase().trim();
let h = this.style.height.toLowerCase().trim();
if (w.endsWith("px") && h.endsWith("px")) {
w = parseFloat(w.slice(0, w.length - 2).trim());
h = parseFloat(h.slice(0, h.length - 2).trim());
if (w !== this.size[0] || h !== this.size[1]) {
this.on_resize([this.size[0], this.size[1]], [w, h]);
this.size[0] = w;
this.size[1] = h;
}
}
}
getBoolAttribute(attr, defaultval = false) {
if (!this.hasAttribute(attr)) {
return defaultval;
}
let ret = this.getAttribute(attr);
if (typeof ret === "number") {
return !!ret;
} else if (typeof ret === "string") {
ret = ret.toLowerCase().trim();
ret = ret === "true" || ret === "1" || ret === "yes";
}
return !!ret;
}
updateSize() {
if (this.getBoolAttribute("inherit-scale") || !this.fullScreen || !cconst.autoSizeUpdate) {
this.checkCSSSize();
return;
}
let width = window.innerWidth;
let height = window.innerHeight;
let ratio = window.outerHeight/window.innerHeight;
let scale = visualViewport.scale;
let pad = 4;
width = visualViewport.width*scale - pad;
height = visualViewport.height*scale - pad;
let ox = visualViewport.offsetLeft;
let oy = visualViewport.offsetTop;
if (cconst.DEBUG.customWindowSize) {
let s = cconst.DEBUG.customWindowSize;
width = s.width;
height = s.height;
ox = 0;
oy = 0;
window._DEBUG = cconst.DEBUG;
}
let key = this._calcSizeKey(width, height, ox, oy, devicePixelRatio, scale);
/* CSS IS EVIL! WHY DOES BODY HAVE A MARGIN? */
document.body.style.margin = document.body.style.padding = "0px";
document.body.style["transform-origin"] = "top left";
document.body.style["transform"] = `translate(${ox}px,${oy}px) scale(${1.0/scale})`;
//document.body.style["transform"] = `scale(${1.0 / scale}, ${1.0 / scale})`; // translate(${ox*scale2}px, ${oy*scale2}px)`;
if (key !== this._last_ckey1) {
//console.log("resizing", key, this._last_ckey1);
this._last_ckey1 = key;
this.on_resize(this.size, [width, height], false);
this.on_resize(this.size, this.size, false);
let scale = visualViewport.scale;
this.regenBorders();
this.setCSS();
this.completeUpdate();
}
}
listen(args = {updateSize: true}) {
ui_menu.setWranglerScreen(this);
let ctx = this.ctx;
startEvents(() => ctx.screen);
if (this.listen_timer !== undefined) {
return; //already listening
}
this._do_updateSize = args.updateSize !== undefined ? args.updateSize : true;
this.listen_timer = window.setInterval(() => {
if (this.isDead()) {
console.log("dead screen");
this.unlisten();
return;
}
this.update();
}, 150);
}
_calcSizeKey(w, h, x, y, dpi, scale) {
if (arguments.length !== 6) {
throw new Error("eek");
}
let s = "";
for (let i = 0; i < arguments.length; i++) {
s += arguments[i].toFixed(0) + ":";
}
return s;
}
_ondestroy() {
if (ui_menu.getWranglerScreen() === this) {
//ui_menu.setWranglerScreen(undefined);
}
this.unlisten();
//unlike other ondestroy functions, this one physically dismantles the DOM tree
let recurse = (n, second_pass, parent) => {
if (n.__pass === second_pass) {
console.warn("CYCLE IN DOM TREE!", n, parent);
return;
}
n.__pass = second_pass;
n._forEachChildWidget(n2 => {
if (n === n2)
return;
recurse(n2, second_pass, n);
try {
if (!second_pass && !n2.__destroyed) {
n2.__destroyed = true;
n2._ondestroy();
}
} catch (error) {
print_stack(error);
console.log("failed to exectue an ondestroy callback");
}
n2.__destroyed = true;
try {
if (second_pass) {
n2.remove();
}
} catch (error) {
print_stack(error);
console.log("failed to remove element after ondestroy callback");
}
});
};
let id = ~~(Math.random()*1024*1024);
recurse(this, id);
recurse(this, id + 1);
}
destroy() {
this._ondestroy();
}
clear() {
this._ondestroy();
this.sareas = [];
this.sareas.active = undefined;
for (let child of list(this.childNodes)) {
child.remove();
}
for (let child of list(this.shadow.childNodes)) {
child.remove();
}
}
_test_save() {
let obj = JSON.parse(JSON.stringify(this));
console.log(JSON.stringify(this));
this.loadJSON(obj);
}
loadJSON(obj, schedule_resize = false) {
this.clear();
super.loadJSON();
for (let sarea of obj.sareas) {
let sarea2 = UIBase.createElement("screenarea-x");
sarea2.ctx = this.ctx;
sarea2.screen = this;
this.appendChild(sarea2);
sarea2.loadJSON(sarea);
}
this.regenBorders();
this.setCSS();
if (schedule_resize) {
window.setTimeout(() => {
this.on_resize(this.size, [window.innerWidth, window.innerHeight]);
}, 50);
}
}
toJSON() {
let ret = {
sareas: this.sareas
};
ret.size = this.size;
ret.idgen = this.idgen;
return Object.assign(super.toJSON(), ret);
}
getHotKey(toolpath) {
let test = (keymap) => {
for (let hk of keymap) {
if (typeof hk.action != "string")
continue;
if (hk.action.trim().startsWith(toolpath.trim())) {
return hk;
}
}
};
let ret = test(this.keymap);
if (ret)
return ret;
if (this.sareas.active && this.sareas.active.keymap) {
let area = this.sareas.active.area;
for (let keymap of area.getKeyMaps()) {
ret = test(keymap);
if (ret)
return ret;
}
}
if (ret === undefined) {
//just to be safe, check all areas in case the
//context is confused as to which area is currently "active"
for (let sarea of this.sareas) {
let area = sarea.area;
for (let keymap of area.getKeyMaps()) {
ret = test(keymap);
if (ret) {
return ret;
}
}
}
}
return undefined;
}
addEventListener(type, cb, options) {
if (type === "resize") {
this._resize_callbacks.push(cb);
} else {
return super.addEventListener(type, cb, options);
}
}
removeEventListener(type, cb, options) {
if (type === "resize") {
if (this._resize_callbacks.indexOf(cb) >= 0)
this._resize_callbacks.remove(cb);
} else {
return super.removeEventListener(type, cb, options);
}
}
execKeyMap(e) {
let handled = false;
if (window.DEBUG && window.DEBUG.keymap) {
console.warn("execKeyMap called", e.keyCode, document.activeElement.tagName);
}
if (this.sareas.active) {
let area = this.sareas.active.area;
if (!area) {
return;
}
area.push_ctx_active();
for (let keymap of area.getKeyMaps()) {
if (keymap === undefined) {
continue;
}
if (keymap.handle(this.ctx, e)) {
handled = true;
break;
}
}
area.pop_ctx_active();
}
handled = handled || this.keymap.handle(this.ctx, e);
if (!handled && this.testAllKeyMaps) {
for (let sarea of this.sareas) {
if (handled) {
break;
}
sarea.area.push_ctx_active();
for (let keymap of sarea.area.getKeyMaps()) {
if (keymap.handle(this.ctx, e)) {
handled = true;
break;
}
}
sarea.area.pop_ctx_active();
}
}
return handled;
}
calcTabOrder() {
let nodes = [];
let visit = {};
let rec = (n) => {
let bad = n.tabIndex < 0 || n.tabIndex === undefined || n.tabIndex === null;
bad = bad || !(n instanceof UIBase);
if (n._id in visit || n.hidden) {
return;
}
visit[n._id] = 1;
if (!bad) {
n.__pos = n.getClientRects()[0];
if (n.__pos) {
nodes.push(n);
}
}
n._forEachChildWidget((n2) => {
rec(n2);
});
};
for (let sarea of this.sareas) {
rec(sarea);
}
for (let popup of this._popups) {
rec(popup);
}
//console.log("nodes2", nodes2);
for (let i = 0; i < nodes.length; i++) {
let n = nodes[i];
n.tabIndex = i + 1;
//console.log(n.tabIndex);
}
}
drawUpdate() {
if (window.redraw_all !== undefined) {
window.redraw_all();
}
}
update() {
let move = [];
for (let child of this.childNodes) {
if (child instanceof ScreenArea) {
move.push(child);
}
}
for (let child of move) {
console.warn("moved screen area to shadow");
HTMLElement.prototype.remove.call(child);
this.shadow.appendChild(child);
}
if (this._do_updateSize) {
this.updateSize();
}
if (this.needsTabRecalc) {
this.needsTabRecalc = false;
this.calcTabOrder();
}
outer: for (let sarea of this.sareas) {
for (let b of sarea._borders) {
let movable = this.isBorderMovable(b);
if (movable !== b.movable) {
console.log("detected change in movable borders");
this.regenBorders();
break outer;
}
}
}
if (this._update_gen) {
let ret;
/*
if (cconst.DEBUG.debugUIUpdatePerf) {
for (ret = this._update_gen.next(); !ret.done; ret = this._update_gen.next()) {}
this._update_gen = this.update_intern();
return;
}
//*/
try {
ret = this._update_gen.next();
} catch (error) {
if (!(error instanceof DataPathError)) {
util.print_stack(error);
console.log("error in update_intern tasklet");
}
return;
}
if (ret !== undefined && ret.done) {
this._update_gen = undefined;
}
} else {
this._update_gen = this.update_intern();
}
}
purgeUpdateStack() {
this._update_gen = undefined;
purgeUpdateStack();
}
completeSetCSS() {
let rec = (n) => {
n.setCSS();
if (n.packflag & PackFlags.NO_UPDATE) {
return;
}
n._forEachChildWidget((c) => {
rec(c);
})
}
rec(this);
}
completeUpdate() {
for (let step of this.update_intern()) {
}
}
updateScrollStyling() {
let s = theme.scrollbars;
if (!s || !s.color) return;
let key = "" + s.color + ":" + s.color2 + ":" + s.border + ":" + s.contrast + ":" + s.width;
if (key !== this._last_scrollstyle_key) {
this._last_scrollstyle_key = key;
//console.log("updating scrollbar styling");
this.mergeGlobalCSS(styleScrollBars(s.color, s.color2, s.contrast, s.width, s.border, "*"));
}
}
//XXX race condition warning
update_intern() {
this.updateScrollStyling();
let popups = this._popups;
let cssText = "";
let sheet = this.globalCSS.sheet;
if (sheet) {
for (let rule of sheet.rules) {
cssText += rule.cssText + "\n";
}
window.cssText = cssText
}
let cssTextHash = util.strhash(cssText);
if (this.needsBorderRegen) {
this.needsBorderRegen = false;
this.regenBorders();
}
super.update();
let this2 = this;
//ensure each area has proper ctx set
for (let sarea of this.sareas) {
sarea.ctx = this.ctx;
}
return (function* () {
let stack = update_stack;
stack.cur = 0;
let lastn = this2;
function push(n) {
stack[stack.cur++] = n;
}
function pop(n) {
if (stack.cur < 0) {
throw new Error("Screen.update(): stack overflow!");
}
return stack[--stack.cur];
}
let ctx = this2.ctx;
let SCOPE_POP = Symbol("pop");
let AREA_CTX_POP = Symbol("pop2");
let scopestack = [];
let areastack = [];
let t = util.time_ms();
push(this2);
for (let p of popups) {
push(p);
}
while (stack.cur > 0) {
let n = pop();
if (n === undefined) {
//console.log("eek!", stack.length);
continue;
} else if (n === SCOPE_POP) {
scopestack.pop();
continue;
} else if (n === AREA_CTX_POP) {
//console.log("POP", areastack[areastack.length-1].constructor.name);
areastack.pop().pop_ctx_active(ctx, true);
continue;
}
if (n instanceof Area) {
//console.log("PUSH", n.constructor.name);
areastack.push(n);
n.push_ctx_active(ctx, true);
push(AREA_CTX_POP);
}
if (!n.hidden && n !== this2 && n instanceof UIBase) {
if (!n._ctx) {
n._ctx = ctx;
}
if (n._screenStyleUpdateHash !== cssTextHash) {
n._screenStyleTag.textContent = cssText;
n._screenStyleUpdateHash = cssTextHash;
}
if (scopestack.length > 0 && scopestack[scopestack.length - 1]) {
n.parentWidget = scopestack[scopestack.length - 1];
//if (n.parentWidget && n._useDataPathUndo === undefined && n.parentWidget._useDataPathUndo !== undefined) {
// n._useDataPathUndo = n.parentWidget._useDataPathUndo;
//}
}
n.update();
}
if (util.time_ms() - t > 20) {
yield;
t = util.time_ms();
}
for (let n2 of n.childNodes) {
if (!(n2 instanceof UIBase) || !(n2.packflag & PackFlags.NO_UPDATE)) {
push(n2);
}
}
if (n.shadow === undefined) {
continue;
}
for (let n2 of n.shadow.childNodes) {
if (!(n2 instanceof UIBase) || !(n2.packflag & PackFlags.NO_UPDATE)) {
push(n2);
}
}
if (n instanceof UIBase) {
if (!(n.packflag & PackFlags.NO_UPDATE)) {
scopestack.push(n);
push(SCOPE_POP);
}
}
}
})();
}
//load pos/size from screenverts
loadFromVerts() {
let old = [0, 0];
for (let sarea of this.sareas) {
old[0] = sarea.size[0];
old[1] = sarea.size[1];
sarea.loadFromVerts();
sarea.on_resize(old);
sarea.setCSS();
}
this.setCSS();
}
/** merges sarea into the screen area opposite to sarea*/
collapseArea(sarea, border) {
let sarea2;
if (!border) {
for (let b of sarea._borders) {
let sarea2 = b.getOtherSarea(sarea);
if (sarea2 && !b.locked) {
border = b;
break;
}
}
} else if (border.locked) {
console.warn("Cannot remove screen border");
}
console.warn("SAREA2", border, sarea2, sarea2 !== sarea);
if (border) {
sarea2 = border.getOtherSarea(sarea);
if (!sarea2) {
console.error("Error merging sarea");
return;
}
let size1 = new Vector2(sarea.pos).add(sarea.size);
let size2 = new Vector2(sarea2.pos).add(sarea2.size);
sarea2.pos.min(sarea.pos);
sarea2.size.load(size1).max(size2).sub(sarea2.pos);
sarea2.loadFromPosSize();
}
this.sareas.remove(sarea);
sarea.remove();
this.regenScreenMesh();
this._internalRegenAll();
return this;
}
splitArea(sarea, t = 0.5, horiz = true) {
let w = sarea.size[0], h = sarea.size[1];
let x = sarea.pos[0], y = sarea.size[1];
let s1, s2;
if (!horiz) {
s1 = sarea;
if (s1.ctx === undefined) {
s1.ctx = this.ctx;
}
s2 = s1.copy(this);
s1.size[0] *= t;
s2.size[0] *= (1.0 - t);
s2.pos[0] += w*t;
} else {
s1 = sarea;
if (s1.ctx === undefined) {
s1.ctx = this.ctx;
}
s2 = s1.copy(this);
s1.size[1] *= t;
s2.size[1] *= (1.0 - t);
s2.pos[1] += h*t;
}
s2.ctx = this.ctx;
this.appendChild(s2);
s1.on_resize(s1.size);
s2.on_resize(s2.size);
this.regenBorders();
this.solveAreaConstraints();
s1.setCSS();
s2.setCSS();
this.setCSS();
//XXX not sure if this is right place to do this or really necassary
if (s2.area !== undefined)
s2.area.onadd();
return s2;
}
setCSS() {
if (!this.getBoolAttribute("inherit-scale")) {
this.style["width"] = this.size[0] + "px";
this.style["height"] = this.size[1] + "px";
}
this.style["overflow"] = "hidden";
//call setCSS on borders
for (let key in this._edgemap) {
let b = this._edgemap[key];
b.setCSS();
}
}
regenScreenMesh(snapLimit = SnapLimit) {
this.snapLimit = snapLimit;
this.regenBorders();
}
regenBorders_stage2() {
for (let b of this.screenborders) {
b.halfedges = []
}
function hashHalfEdge(border, sarea) {
return border._id + ":" + sarea._id;
}
function has_he(border, border2, sarea) {
for (let he of border.halfedges) {
if (border2 === he.border && sarea === he.sarea) {
return true;
}
}
return false;
}
for (let b1 of this.screenborders) {
for (let sarea of b1.sareas) {
let he = new ScreenHalfEdge(b1, sarea);
b1.halfedges.push(he);
}
let axis = b1.horiz ? 1 : 0;
let min = Math.min(b1.v1[axis], b1.v2[axis]);
let max = Math.max(b1.v1[axis], b1.v2[axis]);
for (let b2 of this.walkBorderLine(b1)) {
if (b1 === b2) {
continue;
}
let ok = b2.v1[axis] >= min && b2.v1[axis] <= max;
ok = ok || (b2.v2[axis] >= min && b2.v2[axis] <= max);
for (let sarea of b2.sareas) {
let ok2 = ok && !has_he(b2, b1, sarea);
if (ok2) {
let he2 = new ScreenHalfEdge(b2, sarea);
b1.halfedges.push(he2);
}
}
}
}
for (let b of this.screenborders) {
let movable = true;
for (let sarea of b.sareas) {
movable = movable && this.isBorderMovable(b);
}
b.movable = movable;
}
}
hasBorder(b) {
return b._id in this._idmap;
}
killScreenVertex(v) {
this.screenverts.remove(v);
delete this._edgemap[ScreenVert.hash(v, undefined, this.snapLimit)];
delete this._idmap[v._id];
return this;
}
freeBorder(b, sarea) {
if (b.sareas.indexOf(sarea) >= 0) {
b.sareas.remove(sarea);
}
let dels = [];
for (let he of b.halfedges) {
if (he.sarea === sarea) {
dels.push([b, he]);
}
for (let he2 of he.border.halfedges) {
if (he2 === he)
continue;
if (he2.sarea === sarea) {
dels.push([he.border, he2]);
}
}
}
for (let d of dels) {
if (d[0].halfedges.indexOf(d[1]) < 0) {
console.warn("Double remove detected; use util.set?");
continue;
}
d[0].halfedges.remove(d[1]);
}
if (b.sareas.length === 0) {
this.killBorder(b);
}
}
killBorder(b) {
console.log("killing border", b._id, b);
if (this.screenborders.indexOf(b) < 0) {
console.log("unknown border", b);
b.remove();
return;
}
this.screenborders.remove(b);
let del = [];
for (let he of b.halfedges) {
if (he === he2)
continue;
for (let he2 of he.border.halfedges) {
if (he2.border === b) {
del.push([he.border, he2]);
}
}
}
for (let d of del) {
d[0].halfedges.remove(d[1]);
}
delete this._edgemap[ScreenBorder.hash(b.v1, b.v2)];
delete this._idmap[b._id];
b.v1.borders.remove(b);
b.v2.borders.remove(b);
if (b.v1.borders.length === 0) {
this.killScreenVertex(b.v1);
}
if (b.v2.borders.length === 0) {
this.killScreenVertex(b.v2);
}
b.remove();
return this;
}
//XXX rename to regenScreenMesh
regenBorders() {
for (let b of this.screenborders) {
b.remove();
HTMLElement.prototype.remove.call(b);
}
this._idmap = {};
this.screenborders = [];
this._edgemap = {};
this._vertmap = {};
this.screenverts = []
for (let sarea of this.sareas) {
if (sarea.hidden) continue;
sarea.makeBorders(this);
}
for (let key in this._edgemap) {
let b = this._edgemap[key];
b.setCSS();
}
this.regenBorders_stage2();
this._recalcAABB();
for (let b of this.screenborders) {
b.outer = this.isBorderOuter(b);
b.movable = this.isBorderMovable(b);
b.setCSS();
}
this.updateDebugBoxes();
}
_get_debug_overlay() {
if (!this._debug_overlay) {
this._debug_overlay = UIBase.createElement("overdraw-x");
let s = this._debug_overlay;
s.startNode(this, this);
}
return this._debug_overlay;
}
updateDebugBoxes() {
if (cconst.DEBUG.screenborders) {
let overlay = this._get_debug_overlay();
overlay.clear();
for (let b of this.screenborders) {
overlay.line(b.v1, b.v2, "red");
}
let del = [];
for (let child of document.body.childNodes) {
if (child.getAttribute && child.getAttribute("class") === "__debug") {
del.push(child);
}
}
for (let n of del) {
n.remove();
}
let box = (x, y, s, text, color = "red") => {
x -= s*0.5;
y -= s*0.5;
x = Math.min(Math.max(x, 0.0), this.size[0] - s);
y = Math.min(Math.max(y, 0.0), this.size[1] - s);
let ret = UIBase.createElement("div");
ret.setAttribute("class", "__debug");
ret.style["position"] = UIBase.PositionKey;
ret.style["left"] = x + "px";
ret.style["top"] = y + "px";
ret.style["height"] = s + "px";
ret.style["width"] = s + "px";//"200px";
ret.style["z-index"] = "1000";
ret.style["pointer-events"] = "none";
ret.style["padding"] = ret.style["margin"] = "0px";
ret.style['display'] = "float";
ret.style["background-color"] = color;
document.body.appendChild(ret);
let colors = [
"orange",
"black",
"white",
];
for (let i = 2; i >= 0; i--) {
ret = UIBase.createElement("div");
ret.setAttribute("class", "__debug");
ret.style["position"] = UIBase.PositionKey;
ret.style["left"] = x + "px";
ret.style["top"] = y + "px";
ret.style["height"] = s + "px";
ret.style["width"] = "250px";//"200px";
ret.style["z-index"] = "" + (1005 - i - 1);
ret.style["pointer-events"] = "none";
ret.style["color"] = colors[i];
let w = (i)*2;
ret.style["-webkit-text-stroke-width"] = w + "px";
ret.style["-webkit-text-stroke-color"] = colors[i];
ret.style["text-stroke-width"] = w + "px";
ret.style["text-stroke-color"] = colors[i];
ret.style["padding"] = ret.style["margin"] = "0px";
ret.style['display'] = "float";
ret.style["background-color"] = "rgba(0,0,0,0)";
ret.innerText = "" + text;
document.body.appendChild(ret);
}
};
for (let v of this.screenverts) {
box(v[0], v[1], 10*v.borders.length, "" + v.borders.length, "rgba(255,0,0,0.5)");
}
for (let b of this.screenborders) {
for (let he of b.halfedges) {
let txt = `${he.side}, ${b.sareas.length}, ${b.halfedges.length}`;
let p = new Vector2(b.v1).add(b.v2).mulScalar(0.5);
let size = 10*b.halfedges.length;
let wadd = 25 + size*0.5;
let axis = b.horiz & 1;
if (p[axis] > he.sarea.pos[axis]) {
p[axis] -= wadd;
} else {
p[axis] += wadd;
}
box(p[0], p[1], size, txt, "rgba(155,255,75,0.5)")
}
}
}
}
checkAreaConstraint(sarea, checkOnly = false) {
let min = sarea.minSize, max = sarea.maxSize;
let vs = sarea._verts;
let chg = 0.0;
let mask = 0;
let moveBorder = (sidea, sideb, dh) => {
let b1 = sarea._borders[sidea];
let b2 = sarea._borders[sideb];
let bad = 0;
for (let i = 0; i < 2; i++) {
let b = i ? b2 : b1;
let bad2 = sarea.borderLock & (1<<sidea);
bad2 = bad2 || !b.movable;
bad2 = bad2 || this.isBorderOuter(b);
if (bad2)
bad |= 1<<i;
}
if (bad === 0) {
this.moveBorder(b1, dh*0.5);
this.moveBorder(b2, -dh*0.5);
} else if (bad === 1) {
this.moveBorder(b2, -dh);
} else if (bad === 2) {
this.moveBorder(b1, dh);
} else if (bad === 3) {
//both borders are bad, yet we need to move anyway. . .
//console.warn("got case of two borders being bad");
if (!this.isBorderOuter(b1)) {
this.moveBorder(b1, dh);
} else if (!this.isBorderOuter(b2)) {
this.moveBorder(b2, -dh);
} else {
this.moveBorder(b1, dh*0.5);
this.moveBorder(b2, -dh*0.5);
}
}
};
if (max[0] !== undefined && sarea.size[0] > max[0]) {
let dh = (sarea.size[0] - max[0]);
chg += Math.abs(dh);
mask |= 1;
moveBorder(0, 2, dh);
}
if (min[0] !== undefined && sarea.size[0] < min[0]) {
let dh = (min[0] - sarea.size[0]);
chg += Math.abs(dh);
mask |= 2;
moveBorder(2, 0, dh);
}
if (max[1] !== undefined && sarea.size[1] > max[1]) {
let dh = (sarea.size[1] - max[1]);
chg += Math.abs(dh);
mask |= 4;
moveBorder(3, 1, dh);
}
if (min[1] !== undefined && sarea.size[1] < min[1]) {
let dh = (min[1] - sarea.size[1]);
chg += Math.abs(dh);
mask |= 8;
moveBorder(1, 3, dh);
}
if (sarea.pos[0] + sarea.size[0] > this.size[0]) {
mask |= 16;
let dh = ((this.size[0] - sarea.size[0]) - sarea.pos[0]);
chg += Math.abs(dh);
if (sarea.floating) {
sarea.pos[0] = this.size[0] - sarea.size[0];
sarea.loadFromPosSize();
} else {
this.moveBorder(sarea._borders[0], dh);
this.moveBorder(sarea._borders[2], dh);
}
}
if (chg === 0.0) {
return false;
}
return mask;
}
walkBorderLine(b) {
let visit = new util.set();
let ret = [b];
visit.add(b);
let rec = (b, v) => {
for (let b2 of v.borders) {
if (b2 === b) {
continue;
}
if (b2.horiz === b.horiz && !visit.has(b2)) {
visit.add(b2);
ret.push(b2);
rec(b2, b2.otherVertex(v));
}
}
}
rec(b, b.v1);
let ret2 = ret;
ret = [];
rec(b, b.v2);
ret2.reverse();
return ret2.concat(ret);
}
moveBorderWithoutVerts(halfedge, df) {
let side = halfedge.side;
let sarea = halfedge.sarea;
switch (side) {
case 0:
sarea.pos[0] += df;
sarea.size[0] -= df;
break;
case 1:
sarea.size[1] += df;
break;
case 2:
sarea.size[0] += df;
break;
case 3:
sarea.pos[1] += df;
sarea.size[1] -= df;
break;
}
}
moveBorder(b, df, strict = true) {
return this.moveBorderSimple(b, df, strict);
}
moveBorderSimple(b, df, strict = true) {
let axis = b.horiz & 1;
let axis2 = axis ^ 1;
let min = Math.min(b.v1[axis2], b.v2[axis2]);
let max = Math.max(b.v1[axis2], b.v2[axis2]);
let test = (v) => {
return v[axis2] >= min && v[axis2] <= max;
};
let vs = new util.set();
for (let b2 of this.walkBorderLine(b)) {
if (strict && !test(b2.v1) && !test(b2.v2)) {
return false;
}
vs.add(b2.v1);
vs.add(b2.v2);
}
for (let v of vs) {
v[axis] += df;
}
for (let v of vs) {
for (let b of v.borders) {
for (let sarea of b.sareas) {
sarea.loadFromVerts();
}
}
}
return true;
}
moveBorderUnused(b, df, strict = true) {
if (!b) {
console.warn("missing border");
return false;
}
let axis = b.horiz & 1;
let vs = new util.set();
let visit = new util.set();
let axis2 = axis ^ 1;
let min = Math.min(b.v1[axis2], b.v2[axis2]);
let max = Math.max(b.v1[axis2], b.v2[axis2]);
let test = (v) => {
return v[axis2] >= min && v[axis2] <= max;
};
let first = true;
let found = false;
let halfedges = new util.set();
let borders = new util.set();
for (let b2 of this.walkBorderLine(b)) {
/*
if (first) {
first = false;
df = Math.max(Math.abs(df), FrameManager_mesh.SnapLimit) * Math.sign(df);
}
found = true;
for (let sarea of b2.sareas) {
halfedges.add(new ScreenHalfEdge(b2, sarea))
}
vs.add(b2.v1);
vs.add(b2.v2);
continue;
//*/
if (!strict) {
vs.add(b2.v1);
vs.add(b2.v2);
continue;
}
let t1 = test(b2.v1), t2 = test(b2.v2);
if (!t1 || !t2) {
found = true;
if (first) {
first = false;
df = Math.max(Math.abs(df), FrameManager_mesh.SnapLimit)*Math.sign(df);
}
}
if (!t1 && !t2) {
continue;
}
borders.add(b2);
//make dummy half edges to keep track of border/sarea pairs
//and especailly what the border side is
for (let sarea of b2.sareas) {
halfedges.add(new ScreenHalfEdge(b2, sarea))
}
vs.add(b2.v1);
vs.add(b2.v2);
}
for (let b2 of this.walkBorderLine(b)) {
if (borders.has(b2)) {
continue;
}
for (let he of b2.halfedges) {
borders.remove(he.border);
if (halfedges.has(he)) {
halfedges.remove(he);
}
}
}
for (let v of vs) {
let ok = v[axis2] >= min && v[axis2] <= max;
if (!ok && strict) {
// return false;
}
}
if (!found || !strict) {
for (let v of vs) {
v[axis] += df;
}
} else {
let borders = new util.set();
for (let he of halfedges) {
borders.add(he.border);
this.moveBorderWithoutVerts(he, df);
}
for (let he of halfedges) {
he.sarea.loadFromPosSize();
}
for (let b of borders) {
let sareas = b.sareas.slice(0, b.sareas.length);
this.killBorder(b);
for (let sarea of sareas) {
sarea.loadFromPosSize();
}
}
return halfedges.length > 0;
}
for (let sarea of b.sareas) {
sarea.loadFromVerts();
}
for (let he of b.halfedges) {
he.sarea.loadFromVerts();
for (let sarea of he.border.sareas) {
sarea.loadFromVerts();
for (let b2 of sarea._borders) {
b2.setCSS();
}
}
}
b.setCSS();
return true;
}
solveAreaConstraints(snapArgument = true) {
let repeat = false;
let found = false;
let time = util.time_ms();
for (let i = 0; i < 10; i++) {
repeat = false;
for (let sarea of this.sareas) {
if (sarea.hidden) continue;
repeat = repeat || this.checkAreaConstraint(sarea);
}
found = found || repeat;
if (repeat) {
for (let sarea of this.sareas) {
sarea.loadFromVerts();
}
this.snapScreenVerts(snapArgument);
} else {
break;
}
}
if (found) {
this.snapScreenVerts(snapArgument);
if (cconst.DEBUG.areaConstraintSolver) {
time = util.time_ms() - time;
console.log(`enforced area constraint ${time.toFixed(2)}ms`);
}
this._recalcAABB();
this.setCSS();
}
}
snapScreenVerts(fitToSize = true) {
let this2 = this;
function* screenverts() {
for (let v of this2.screenverts) {
let ok = 0;
for (let sarea of v.sareas) {
if (!(sarea.flag & AreaFlags.INDEPENDENT)) {
ok = 1;
}
}
if (ok) {
yield v;
}
}
}
let mm = new math.MinMax(2);
for (let v of screenverts()) {
mm.minmax(v);
}
let min = mm.min, max = mm.max;
//snap(min);
//snapi(max);
if (fitToSize) {
//fit entire screen to, well, the entire screen (size)
let vec = new Vector2(max).sub(min);
let sz = new Vector2(this.size);
sz.div(vec);
for (let v of screenverts()) {
v.sub(min).mul(sz);
//snap(v.sub(min).mul(sz));//.add(this.pos);
}
for (let v of screenverts()) {
v[0] += this.pos[0];
v[1] += this.pos[1];
}
//this.pos.zero();
} else {
for (let v of screenverts()) {
//snap(v);
}
[min, max] = this._recalcAABB();
//snap(min);
//snapi(max);
this.size.load(max).sub(min);
//this.pos.zero();
//this.pos.load(min);
}
let found = 1;
for (let sarea of this.sareas) {
if (sarea.hidden) continue;
let old = new Vector2(sarea.size);
let oldpos = new Vector2(sarea.pos);
sarea.loadFromVerts();
found = found || old.vectorDistance(sarea.size) > 1;
found = found || oldpos.vectorDistance(sarea.pos) > 1;
sarea.on_resize(old);
}
if (found) {
//this.regenBorders();
this._recalcAABB();
this.setCSS();
}
}
on_resize(oldsize, newsize = this.size, _set_key = true) {
//console.warn("resizing");
if (_set_key) {
this._last_ckey1 = this._calcSizeKey(newsize[0], newsize[1], this.pos[0], this.pos[1], devicePixelRatio, visualViewport.scale);
}
let ratio = [newsize[0]/oldsize[0], newsize[1]/oldsize[1]];
let offx = this.pos[0] - this.oldpos[0];
let offy = this.pos[1] - this.oldpos[1];
this.oldpos.load(this.pos);
//console.log("resize!", ratio);
for (let v of this.screenverts) {
v[0] *= ratio[0];
v[1] *= ratio[1];
v[0] += offx;
v[1] += offy;
}
let min = [1e17, 1e17], max = [-1e17, -1e17];
let olds = [];
for (let sarea of this.sareas) {
olds.push([sarea.size[0], sarea.size[1]]);
sarea.loadFromVerts();
}
this.size[0] = newsize[0];
this.size[1] = newsize[1];
this.snapScreenVerts();
this.solveAreaConstraints();
this._recalcAABB();
let i = 0;
for (let sarea of this.sareas) {
sarea.on_resize(sarea.size, olds[i]);
sarea.setCSS();
i++;
}
this.regenBorders();
this.setCSS();
this.calcTabOrder();
this._fireResizeCB(oldsize);
}
_fireResizeCB(oldsize = this.size) {
for (let cb of this._resize_callbacks) {
cb(oldsize);
}
}
getScreenVert(pos, added_id = "", floating = false) {
let key = ScreenVert.hash(pos, added_id, this.snapLimit);
if (floating || !(key in this._vertmap)) {
let v = new ScreenVert(pos, this.idgen++, added_id);
this._vertmap[key] = v;
this._idmap[v._id] = v;
this.screenverts.push(v);
}
return this._vertmap[key];
}
isBorderOuter(border) {
let sides = 0;
for (let he of border.halfedges) {
sides |= 1<<he.side;
}
let bits = 0;
for (let i = 0; i < 4; i++) {
bits += (sides & (1<<i)) ? 1 : 0;
}
let ret = bits < 2;
let floating = false;
for (let sarea of border.sareas) {
floating = floating || sarea.floating;
}
if (floating) {
//check if border is on screen limits
let axis = border.horiz ? 1 : 0;
ret = Math.abs(border.v1[axis] - this.pos[axis]) < 4;
ret = ret || Math.abs(border.v1[axis] - this.pos[axis] - this.size[axis]) < 4;
}
border.outer = ret;
return ret;
}
isBorderMovable(b, limit = 5) {
if (this.allBordersMovable)
return true;
for (let he of b.halfedges) {
if (he.sarea.borderLock & (1<<he.side)) {
return false;
}
}
let ok = !this.isBorderOuter(b);
for (let sarea of b.sareas) {
if (sarea.floating) {
ok = true;
break;
}
}
return ok;
}
getScreenBorder(sarea, v1, v2, side) {
let suffix = sarea._get_v_suffix();
if (!(v1 instanceof ScreenVert)) {
v1 = this.getScreenVert(v1, suffix);
}
if (!(v2 instanceof ScreenVert)) {
v2 = this.getScreenVert(v2, suffix);
}
let hash = ScreenBorder.hash(v1, v2);
if (!(hash in this._edgemap)) {
let sb = this._edgemap[hash] = UIBase.createElement("screenborder-x");
sb._hash = hash;
sb.screen = this;
sb.v1 = v1;
sb.v2 = v2;
sb._id = this.idgen++;
v1.borders.push(sb);
v2.borders.push(sb);
sb.ctx = this.ctx;
this.screenborders.push(sb);
this.appendChild(sb);
sb.setCSS();
this._edgemap[hash] = sb;
this._idmap[sb._id] = sb;
}
return this._edgemap[hash];
}
minmaxArea(sarea, mm = undefined) {
if (mm === undefined) {
mm = new math.MinMax(2);
}
for (let b of sarea._borders) {
mm.minmax(b.v1);
mm.minmax(b.v2);
}
return mm;
}
//does sarea1 border sarea2?
areasBorder(sarea1, sarea2) {
for (let b of sarea1._borders) {
for (let sa of b.sareas) {
if (sa === sarea2)
return true;
}
}
return false;
}
//regenerates borders, sets css and calls this.update
replaceArea(dst, src) {
if (dst === src)
return;
src.pos[0] = dst.pos[0];
src.pos[1] = dst.pos[1];
src.size[0] = dst.size[0];
src.size[1] = dst.size[1];
src.floating = dst.floating;
src._borders = dst._borders;
src._verts = dst._verts;
if (this.sareas.indexOf(src) < 0) {
this.sareas.push(src);
this.shadow.appendChild(src);
}
if (this.sareas.active === dst) {
this.sareas.active = src;
}
//this.sareas.remove(dst);
//dst.remove();
this.sareas.remove(dst);
dst.remove();
this.regenScreenMesh();
this.snapScreenVerts();
this._updateAll();
}
_internalRegenAll() {
this.snapScreenVerts();
this._recalcAABB();
this.calcTabOrder();
this.setCSS();
this.completeUpdate();
this.completeSetCSS();
this.completeUpdate();
}
_updateAll() {
for (let sarea of this.sareas) {
sarea.setCSS();
}
this.setCSS();
this.update();
}
removeArea(sarea) {
if (this.sareas.indexOf(sarea) < 0) {
console.warn(sarea, "<- Warning: tried to remove unknown area");
return;
}
this.sareas.remove(sarea);
sarea.remove();
for (let i = 0; i < 2; i++) {
this.snapScreenVerts();
this.regenScreenMesh();
}
this._updateAll();
this.drawUpdate();
}
appendChild(child) {
/*
if (child instanceof UIBase) {
if (child._useDataPathUndo === undefined) {
child.useDataPathUndo = this.useDataPathUndo;
}
}*/
if (child instanceof ScreenArea.ScreenArea) {
child.screen = this;
child.ctx = this.ctx;
child.parentWidget = this;
this.sareas.push(child);
if (child.size.dot(child.size) === 0) {
child.size[0] = this.size[0];
child.size[1] = this.size[1];
}
if (!child._has_evts) {
child._has_evts = true;
let onfocus = (e) => {
this.sareas.active = child;
}
let onblur = (e) => {
//XXX this is causing bugs
//if (this.sareas.active === child) {
// this.sareas.active = undefined;
//}
}
child.addEventListener("focus", onfocus);
child.addEventListener("mouseenter", onfocus);
child.addEventListener("blur", onblur);
child.addEventListener("mouseleave", onblur);
}
this.regenBorders();
child.setCSS();
this.drawUpdate();
child._init();
}
return this.shadow.appendChild(child);
//return super.appendChild(child);
}
add(child) {
return this.appendChild(child);
}
hintPickerTool() {
(new FrameManager_ops.ToolTipViewer(this)).start();
}
removeAreaTool(border) {
let tool = new FrameManager_ops.RemoveAreaTool(this, border);
//let tool = new FrameManager_ops.AreaDragTool(this, undefined, this.mpos);
tool.start();
}
moveAttachTool(sarea, mpos = this.mpos, elem, pointerId) {
let tool = new FrameManager_ops.AreaMoveAttachTool(this, sarea, mpos);
tool.start(elem, pointerId);
}
splitTool() {
let tool = new FrameManager_ops.SplitTool(this);
//let tool = new FrameManager_ops.AreaDragTool(this, undefined, this.mpos);
tool.start();
}
areaDragTool(sarea = this.sareas.active) {
if (sarea === undefined) {
console.warn("no active screen area");
return;
}
let mpos = this.mpos;
let tool = new FrameManager_ops.AreaDragTool(this, this.sareas.active, mpos);
tool.start();
}
makeBorders() {
for (let sarea of this.sareas) {
sarea.makeBorders(this);
}
}
cleanupBorders() {
let del = new Set();
for (let b of this.screenborders) {
if (b.halfedges.length === 0) {
del.add(b);
}
}
for (let b of del) {
delete this._edgemap[b._hash];
HTMLElement.prototype.remove.call(b);
}
}
mergeBlankAreas() {
for (let b of this.screenborders) {
if (b.locked) {
continue;
}
let blank, sarea;
for (let he of b.halfedges) {
if (!he.sarea.area) {
blank = he.sarea;
sarea = b.getOtherSarea(blank);
let axis = b.horiz ^ 1;
if (blank && sarea && blank.size[axis] !== sarea.size[axis]) {
blank = sarea = undefined;
}
if (blank && sarea) {
break;
} else {
blank = undefined;
sarea = undefined;
}
}
}
if (blank && sarea && blank !== sarea) {
this.collapseArea(blank, b);
}
}
this.cleanupBorders();
}
floatArea(area) {
let sarea = area.parentWidget;
/* already floating? */
if (sarea.floating) {
return sarea;
}
sarea.editors.remove(area);
delete sarea.editormap[area.constructor.define().areaname];
sarea.area = undefined;
HTMLElement.prototype.remove.call(area);
let sarea2 = UIBase.createElement("screenarea-x", true);
sarea2.floating = true;
sarea2.pos = new Vector2(sarea.pos);
sarea2.pos.addScalar(5);
sarea2.size = new Vector2(sarea.size);
sarea2.editors.push(area);
sarea2.editormap[area.constructor.define().areaname] = area;
sarea2.shadow.appendChild(area);
sarea2.area = area;
area.push_ctx_active();
area.pop_ctx_active();
area.pos = sarea2.pos;
area.size = sarea2.size;
area.parentWidget = sarea2;
area.owning_sarea = sarea2;
sarea.flushSetCSS();
sarea.flushUpdate();
sarea2.flushSetCSS();
sarea2.flushUpdate();
this.appendChild(sarea2);
if (sarea.editors.length > 0) {
let area2 = sarea.editors[0];
sarea.switch_editor(area2.constructor);
sarea.flushSetCSS();
sarea.flushUpdate();
}
sarea2.loadFromPosSize();
sarea2.bringToFront();
this.mergeBlankAreas();
this.cleanupBorders();
return sarea2;
}
on_keydown(e) {
if (checkForTextBox(this, this.mpos[0], this.mpos[1])) {
console.log("textbox detected");
return;
}
if (!haveModal() && this.execKeyMap(e)) {
e.preventDefault();
return;
}
if (!haveModal() && this.sareas.active !== undefined && this.sareas.active.on_keydown) {
let area = this.sareas.active;
return this.sareas.active.on_keydown(e);
}
}
on_keyup(e) {
if (!haveModal() && this.sareas.active !== undefined && this.sareas.active.on_keyup) {
return this.sareas.active.on_keyup(e);
}
}
on_keypress(e) {
if (!haveModal() && this.sareas.active !== undefined && this.sareas.active.on_keypress) {
return this.sareas.active.on_keypress(e);
}
}
draw() {
for (let sarea of this.sareas) {
sarea.draw();
}
}
afterSTRUCT() {
for (let sarea of this.sareas) {
sarea._ctx = this.ctx;
sarea.afterSTRUCT();
}
}
loadSTRUCT(reader) {
this.clear();
reader(this);
//handle old files that might have saved as simple arrays
this.size = new Vector2(this.size);
let sareas = this.sareas;
this.sareas = [];
/*
let push = this.sareas.push;
this.sareas.push = function(item) {
console.error("this.sareas.push", item);
push.call(this, item);
}
*/
for (let sarea of sareas) {
sarea.screen = this;
sarea.parentWidget = this;
this.appendChild(sarea);
}
this.regenBorders();
this.setCSS();
this.doOnce(() => {
this.loadUIData(this.uidata);
this.uidata = undefined;
});
return this;
}
test_struct(appstate = _appstate) {
let data = [];
//let scripts = nstructjs.write_scripts();
nstructjs.manager.write_object(data, this);
data = new DataView(new Uint8Array(data).buffer);
let screen2 = nstructjs.manager.read_object(data, this.constructor);
screen2.ctx = this.ctx;
for (let sarea of screen2.sareas) {
sarea.screen = screen2;
sarea.ctx = this.ctx;
sarea.area.ctx = this.ctx;
}
let parent = this.parentElement;
this.remove();
appstate.screen = screen2;
parent.appendChild(screen2);
//for (let
screen2.regenBorders();
screen2.update();
screen2.listen();
screen2.doOnce(() => {
screen2.on_resize(screen2.size, [window.innerWidth, window.innerHeight]);
});
console.log(data)
return screen2;
}
saveUIData() {
try {
return ui_base.saveUIData(this, "screen");
} catch (error) {
util.print_stack(error);
console.log("Failed to save UI state data");
}
}
loadUIData(str) {
try {
ui_base.loadUIData(this, str);
} catch (error) {
util.print_stack(error);
console.log("Failed to load UI state data");
}
}
}
Screen.STRUCT = `
pathux.Screen {
size : vec2;
pos : vec2;
sareas : array(pathux.ScreenArea);
idgen : int;
uidata : string | obj.saveUIData();
}
`;
nstructjs.register(Screen);
ui_base.UIBase.internalRegister(Screen);
ScreenArea.setScreenClass(Screen);
_setScreenClass(Screen);
let get_screen_cb;
let _on_keydown;
let start_cbs = [];
let stop_cbs = [];
let keyboardDom = window;
let key_event_opts = undefined;
export function startEvents(getScreenFunc) {
get_screen_cb = getScreenFunc;
if (_events_started) {
return;
}
_events_started = true;
_on_keydown = (e) => {
let screen = get_screen_cb();
return screen.on_keydown(e);
};
window.addEventListener("keydown", _on_keydown, key_event_opts);
for (let cb of start_cbs) {
cb();
}
}
export function stopEvents() {
window.removeEventListener("keydown", _on_keydown, key_event_opts);
_on_keydown = undefined;
_events_started = false;
for (let cb of stop_cbs) {
try {
cb();
} catch (error) {
util.print_stack(error);
}
}
return get_screen_cb;
}
export function setKeyboardDom(dom) {
let started = _events_started;
if (started) {
stopEvents();
}
keyboardDom = dom;
if (started) {
startEvents(get_screen_cb);
}
}
/** Sets options passed to addEventListener() for on_keydown hotkey handler */
export function setKeyboardOpts(opts) {
key_event_opts = opts;
}
export function _onEventsStart(cb) {
start_cbs.push(cb);
}
export function _onEventsStop(cb) {
stop_cbs.push(cb);
}
/*
document.addEventListener("touchstart", (e) => {
e.preventDefault();
}, {capture : true});
document.addEventListener("touchmove", (e) => {
e.preventDefault();
}, {capture : true});
document.addEventListener("scroll", (e) => {
e.preventDefault();
}, {capture : true});
document.addEventListener("resize", (e) => {
e.preventDefault();
}, {capture : true});
document.addEventListener("pointerdown", (e) => {
e.preventDefault();
}, {capture : true});
document.addEventListener("pointerstart", (e) => {
e.preventDefault();
}, {capture : true});
document.addEventListener("pointermove", (e) => {
e.preventDefault();
}, {capture : true});
*/