scripts/widgets/ui_numsliders.js
import {UIBase, drawText} from "../core/ui_base.js";
import {ValueButtonBase} from "./ui_widgets.js";
import cconst from '../config/const.js';
import * as ui_base from "../core/ui_base.js";
import * as units from "../core/units.js";
import {Vector2} from "../path-controller/util/vectormath.js";
import {ColumnFrame} from "../core/ui.js";
import * as util from "../path-controller/util/util.js";
import {
PropTypes, isNumber, PropSubTypes, PropFlags, NumberConstraints, IntProperty
} from "../path-controller/toolsys/toolprop.js";
import {eventWasTouch} from "../path-controller/util/simple_events.js";
import {KeyMap, keymap} from "../path-controller/util/simple_events.js";
import {color2css, css2color} from "../core/ui_theme.js";
import {ThemeEditor} from "./theme_editor.js";
export const sliderDomAttributes = new Set([
"min", "max", "integer", "displayUnit", "baseUnit", "labelOnTop",
"radix", "step", "expRate", "stepIsRelative", "decimalPlaces",
"slideSpeed"
]);
function updateSliderFromDom(dom, slider = dom) {
let redraw = false;
function getbool(attr, prop = attr) {
if (!dom.hasAttribute(attr)) {
return;
}
let v = dom.getAttribute(attr);
let ret = v === null || v.toLowerCase() === "true" || v.toLowerCase === "yes";
let old = slider[prop];
if (old !== undefined && old !== ret) {
redraw = true;
}
slider[prop] = ret;
return ret;
}
function getfloat(attr, prop = attr) {
if (!dom.hasAttribute(attr)) {
return;
}
let v = dom.getAttribute(attr);
let ret = parseFloat(v);
let old = slider[prop];
if (old !== undefined && Math.abs(old - v) < 0.00001) {
redraw = true;
}
slider[prop] = ret;
return ret;
}
function getint(attr, prop = attr) {
if (!dom.hasAttribute(attr)) {
return;
}
let v = ("" + dom.getAttribute(attr)).toLowerCase();
let ret;
if (v === "true") {
ret = true;
} else if (v === "false") {
ret = false;
} else {
ret = parseInt(v);
}
if (isNaN(ret)) {
console.error("bad value " + v);
return 0.0;
}
let old = slider[prop];
if (old !== undefined && Math.abs(old - v) < 0.00001) {
redraw = true;
}
slider[prop] = ret;
return ret;
}
if (dom.hasAttribute("min")) {
slider.range = slider.range || [-1e17, 1e17];
let r = slider.range[0];
slider.range[0] = parseFloat(dom.getAttribute("min"));
redraw = Math.abs(slider.range[0] - r) > 0.0001;
}
if (dom.hasAttribute("max")) {
slider.range = slider.range || [-1e17, 1e17];
let r = slider.range[1];
slider.range[1] = parseFloat(dom.getAttribute("max"));
redraw = redraw || Math.abs(slider.range[1] - r) > 0.0001;
}
if (dom.hasAttribute("displayUnit")) {
let old = slider.displayUnit;
slider.displayUnit = dom.getAttribute("displayUnit").trim();
redraw = redraw || old !== slider.displayUnit;
}
getint("integer", "isInt");
getint("radix");
getint("decimalPlaces");
getbool("labelOnTop");
getbool("stepIsRelative");
getfloat("expRate");
getfloat("step");
return redraw;
}
export const SliderDefaults = {
stepIsRelative: false,
expRate : 1.0 + 1.0/3.0,
radix : 10,
decimalPlaces : 4,
baseUnit : "none",
displayUnit : "none",
slideSpeed : 1.0,
step : 0.1,
}
export function NumberSliderBase(cls = UIBase, skip = new Set(), defaults = SliderDefaults) {
skip = new Set(skip);
return class NumberSliderBase extends cls {
constructor() {
super();
for (let key of NumberConstraints) {
if (skip.has(key)) {
continue;
}
if (key in defaults) {
this[key] = defaults[key];
} else {
this[key] = undefined;
}
}
}
loadConstraints(prop = undefined) {
if (!this.hasAttribute("datapath")) {
return;
}
if (!prop) {
prop = this.getPathMeta(this.ctx, this.getAttribute("datapath"));
}
let loadAttr = (propkey, domkey = key, thiskey = key) => {
if (this.hasAttribute(domkey)) {
this[thiskey] = parseFloat(this.getAttribute(domkey));
} else {
this[thiskey] = prop[propkey];
}
}
for (let key of NumberConstraints) {
let thiskey = key, domkey = key;
if (key === "range") { //handled later
continue;
}
loadAttr(key, domkey, thiskey);
}
let range = prop.range;
if (range && !this.hasAttribute("min")) {
this.range[0] = range[0];
} else if (this.hasAttribute("min")) {
this.range[0] = parseFloat(this.getAttribute("min"));
}
if (range && !this.hasAttribute("max")) {
this.range[1] = range[1];
} else if (this.hasAttribute("max")) {
this.range[1] = parseFloat(this.getAttribute("max"));
}
if (this.getAttribute("integer")) {
let val = this.getAttribute("integer");
val = ("" + val).toLowerCase();
//handles anonymouse <numslider-x integer> case
this.isInt = val === "null" || val === "true" || val === "yes" || val === "1";
} else {
this.isInt = prop instanceof IntProperty;
}
if (this.editAsBaseUnit === undefined) {
if (prop.flag & PropFlags.EDIT_AS_BASE_UNIT) {
this.editAsBaseUnit = true;
} else {
this.editAsBaseUnit = false;
}
}
}
}
}
//use .setAttribute("linear") to disable nonlinear sliding
export class NumSlider extends NumberSliderBase(ValueButtonBase) {
constructor() {
super();
this._highlight = undefined;
this._last_label = undefined;
this.mdown = false;
this.ma = undefined;
this.mpos = new Vector2();
this.start_mpos = new Vector2();
this._last_overarrow = false;
this._name = "";
this._value = 0.0;
this.expRate = SliderDefaults.expRate;
this.vertical = false;
this._last_disabled = false;
this.range = [-1e17, 1e17];
this.isInt = false;
this.editAsBaseUnit = undefined;
this._redraw();
}
get value() {
return this._value;
}
set value(val) {
this.setValue(val);
}
/** Current name label. If set to null label will
* be pulled from the datapath api.*/
get name() {
return this.getAttribute("name") || this._name;
}
/** Current name label. If set to null label will
* be pulled from the datapath api.*/
set name(name) {
if (name === undefined || name === null) {
this.removeAttribute("name");
} else {
this.setAttribute("name", name);
}
}
static define() {
return {
tagname : "numslider-x",
style : "numslider",
parentStyle: "button"
};
}
updateDataPath() {
if (!this.hasAttribute("datapath")) {
return;
}
let prop = this.getPathMeta(this.ctx, this.getAttribute("datapath"));
if (!prop) {
return;
}
let name;
if (this.hasAttribute("name")) {
name = this.getAttribute("name");
} else {
name = "" + prop.uiname;
}
//lazily load constraints, since there's so much
//accessing of DOM attributes
let updateConstraints = false;
if (name !== this._name) {
this._name = name;
this.setCSS();
updateConstraints = true;
}
let val = this.getPathValue(this.ctx, this.getAttribute("datapath"));
if (val !== this._value) {
updateConstraints = true;
/* Note that super.updateDataPath will update .value for us*/
}
if (updateConstraints) {
this.loadConstraints(prop);
}
super.updateDataPath();
}
update() {
if (!!this._last_disabled !== !!this.disabled) {
this._last_disabled = !!this.disabled;
this._redraw();
this.setCSS();
}
super.update(); //calls this.updateDataPath
updateSliderFromDom(this);
}
swapWithTextbox() {
let tbox = UIBase.createElement("textbox-x");
tbox.ctx = this.ctx;
tbox._init();
tbox.decimalPlaces = this.decimalPlaces;
tbox.isInt = this.isInt;
tbox.editAsBaseUnit = this.editAsBaseUnit;
if (this.isInt && this.radix != 10) {
let text = this.value.toString(this.radix);
if (this.radix === 2)
text = "0b" + text;
else if (this.radix === 16)
text += "h";
tbox.text = text;
} else {
tbox.text = units.buildString(this.value, this.baseUnit, this.decimalPlaces, this.displayUnit);
}
this.parentNode.insertBefore(tbox, this);
//this.remove();
this.hidden = true;
//this.dom.hidden = true;
let finish = (ok) => {
tbox.remove();
this.hidden = false;
if (ok) {
let val = tbox.text.trim();
if (this.isInt && this.radix !== 10) {
val = parseInt(val);
} else {
let displayUnit = this.editAsBaseUnit ? undefined : this.displayUnit;
val = units.parseValue(val, this.baseUnit, displayUnit);
}
if (isNaN(val)) {
console.log("Text input error", val, tbox.text.trim(), this.isInt);
this.flash(ui_base.ErrorColors.ERROR);
} else {
this.setValue(val);
if (this.onchange) {
this.onchange(this);
}
}
}
}
tbox.onend = finish;
tbox.focus();
tbox.select();
//this.shadow.appendChild(tbox);
return;
}
bindEvents() {
let dir = this.range && this.range[0] > this.range[1] ? -1 : 1;
this.addEventListener("keydown", (e) => {
switch (e.keyCode) {
case keymap["Left"]:
case keymap["Down"]:
this.setValue(this.value - dir*5*this.step);
break;
case keymap["Up"]:
case keymap["Right"]:
this.setValue(this.value + dir*5*this.step);
break;
}
});
let onmousedown = (e) => {
e.preventDefault();
if (this.disabled) {
this.mdown = false;
e.stopPropagation();
return;
}
if (e.button) {
return;
}
this.mdown = true;
if (e.shiftKey) {
e.preventDefault();
e.stopPropagation();
this.swapWithTextbox();
} else if (this.overArrow(e.x, e.y)) {
this._on_click(e);
} else {
this.dragStart(e);
e.stopPropagation();
}
}
this._on_click = (e) => {
this.setMpos(e);
if (this.disabled) {
e.preventDefault();
e.stopPropagation();
return;
}
let step;
if (step = this.overArrow(e.x, e.y)) {
if (e.shiftKey) {
step *= 0.1;
}
this.setValue(this.value + step);
}
}
this.addEventListener("mousemove", (e) => {
this.setMpos(e);
if (this.mdown && !this._modaldata && this.mpos.vectorDistance(this.start_mpos) > 13) {
this.dragStart(e);
}
});
this.addEventListener("dblclick", (e) => {
this.setMpos(e);
this.mdown = false;
if (this.disabled || this.overArrow(e.x, e.y)) {
e.preventDefault();
e.stopPropagation();
return;
}
e.preventDefault();
e.stopPropagation();
this.swapWithTextbox();
});
this.addEventListener("mousedown", (e) => {
this.setMpos(e);
if (this.disabled) return;
onmousedown(e);
}, {capture: true});
this.addEventListener("mouseup", (e) => {
this.mdown = false;
})
/*
this.addEventListener("touchstart", (e) => {
if (this.disabled) return;
e.x = e.touches[0].screenX;
e.y = e.touches[0].screenY;
this.dragStart(e);
e.preventDefault();
e.stopPropagation();
}, {passive : false});
//*/
//this.addEventListener("mouseup", (e) => {
// return onmouseup(e);
//});
this.addEventListener("mouseover", (e) => {
this.setMpos(e);
if (this.disabled) return;
if (!this._highlight) {
this._highlight = true;
this._repos_canvas();
this._redraw();
}
})
this.addEventListener("blur", (e) => {
this._highlight = false;
this.mdown = false;
});
this.addEventListener("mouseout", (e) => {
this.setMpos(e);
if (this.disabled) return;
this._highlight = false;
this.dom._background = this.getDefault("background-color");
this._repos_canvas();
this._redraw();
})
}
overArrow(x, y) {
let r = this.getBoundingClientRect();
let rwidth, rx;
if (this.vertical) {
rwidth = r.height;
rx = r.y;
x = y;
} else {
rwidth = r.width;
rx = r.x;
}
x -= rx;
let sz = this._getArrowSize();
let szmargin = sz + cconst.numSliderArrowLimit;
let step = this.step || 0.01;
if (this.isInt) {
step = Math.max(step, 1);
}
if (isNaN(step)) {
console.error("NaN step size", "step:", this.step, "numslider:", this._id);
this.flash("red");
step = this.isInt ? 1 : 0.1;
}
if (x < szmargin) {
return -step;
} else if (x > rwidth - szmargin) {
return step;
}
return 0;
}
doRange() {
console.warn("Deprecated: NumSlider.prototype.doRange, use loadConstraints instead!");
this.loadConstraints();
}
setValue(value, fire_onchange = true, setDataPath = true, checkConstraints = true) {
value = Math.min(Math.max(value, this.range[0]), this.range[1]);
this._value = value;
if (this.hasAttribute("integer")) {
this.isInt = true;
}
if (this.isInt) {
this._value = Math.floor(this._value);
}
if (checkConstraints) {
this.loadConstraints();
}
if (setDataPath && this.ctx && this.hasAttribute("datapath")) {
this.setPathValue(this.ctx, this.getAttribute("datapath"), this._value);
}
if (fire_onchange && this.onchange) {
this.onchange(this.value);
}
this._redraw();
}
setMpos(e) {
this.mpos[0] = e.x;
this.mpos[1] = e.y;
if (!this.mdown) {
this.start_mpos[0] = e.x;
this.start_mpos[1] = e.y;
}
let over = this.overArrow(e.x, e.y);
if (over !== this._last_overarrow) {
this._last_overarrow = over;
this._redraw();
}
}
dragStart(e) {
this.mdown = false;
if (this.disabled) return;
if (this.modalRunning) {
console.log("modal already running for numslider", this);
return;
}
this.last_time = util.time_ms();
let last_background = this.dom._background;
let cancel;
this.ma = new util.MovingAvg(eventWasTouch(e) ? 8 : 2);
let startvalue = this.value;
let value = startvalue;
let startx = this.vertical ? e.y : e.x, starty = this.vertical ? e.x : e.y;
let sumdelta = 0;
this.dom._background = this.getDefault("BoxDepressed");
let fire = () => {
if (this.onchange) {
this.onchange(this);
}
}
let handlers = {
on_keydown: (e) => {
switch (e.keyCode) {
case 27: //escape key
cancel(true);
case 13: //enter key
cancel(false);
break;
}
e.preventDefault();
e.stopPropagation();
},
on_mousemove: (e) => {
if (this.disabled) return;
e.preventDefault();
e.stopPropagation();
let x = this.ma.add(this.vertical ? e.y : e.x);
let dx = x - startx;
startx = x;
if (util.time_ms() - this.last_time < 35) {
return;
}
this.last_time = util.time_ms();
if (e.shiftKey) {
dx *= 0.1;
}
dx *= this.vertical ? -1 : 1;
sumdelta += Math.abs(dx);
value += dx*this.step*0.1*this.slideSpeed;
let dvalue = value - startvalue;
let dsign = Math.sign(dvalue);
let expRate = this.expRate;
//if (eventWasTouch(e)) {
// expRate = (1 + expRate)*0.5;
//}
if (!this.hasAttribute("linear")) {
dvalue = Math.pow(Math.abs(dvalue), expRate)*dsign;
}
this.value = startvalue + dvalue;
/*
if (e.shiftKey) {
dx *= 0.1;
this.value = startvalue2 + dx*0.1*this.step*this.slideSpeed;
startvalue3 = this.value;
} else {
startvalue2 = this.value;
this.value = startvalue3 + dx*0.1*this.step*this.slideSpeed;
}*/
this.updateWidth();
this._redraw();
fire();
},
on_mouseup: (e) => {
this.setMpos(e);
this.undoBreakPoint();
cancel(false);
e.preventDefault();
e.stopPropagation();
},
on_mouseout: (e) => {
last_background = this.getDefault("background-color");
e.preventDefault();
e.stopPropagation();
},
on_mouseover: (e) => {
last_background = this.getDefault("BoxHighlight");
e.preventDefault();
e.stopPropagation();
},
on_mousedown: (e) => {
this.popModal();
},
};
//events.pushModal(this.getScreen(), handlers);
this.pushModal(handlers);
cancel = (restore_value) => {
if (restore_value) {
this.value = startvalue;
this.updateWidth();
fire();
}
this.dom._background = last_background; //ui_base.getDefault("background-color");
this._redraw();
this.popModal();
}
/*
cancel = (restore_value) => {
if (restore_value) {
this.value = startvalue;
this.updateWidth();
fire();
}
this.dom._background = last_background; //ui_base.getDefault("background-color");
this._redraw();
console.trace("end");
window.removeEventListener("keydown", keydown, true);
window.removeEventListener("mousemove", mousemove, {captured : true, passive : false});
window.removeEventListener("touchend", touchend, true);
window.removeEventListener("touchmove", touchmove, {captured : true, passive : false});
window.removeEventListener("touchcancel", touchcancel, true);
window.removeEventListener("mouseup", mouseup, true);
this.removeEventListener("mouseover", mouseover, true);
this.removeEventListener("mouseout", mouseout, true);
}
window.addEventListener("keydown", keydown, true);
window.addEventListener("mousemove", mousemove, true);
window.addEventListener("touchend", touchend, true);
window.addEventListener("touchmove", touchmove, {captured : true, passive : false});
window.addEventListener("touchcancel", touchcancel, true);
window.addEventListener("mouseup", mouseup, true);
this.addEventListener("mouseover", mouseover, true);
this.addEventListener("mouseout", mouseout, true);
//*/
}
setCSS() {
//do not call parent class implementation
let dpi = this.getDPI();
let ts = this.getDefault("DefaultText").size*UIBase.getDPI();
let dd = this.isInt ? 5 : this.decimalPlaces + 8;
let label = this._genLabel();
let tw = ui_base.measureText(this, label, {
size: ts,
font: this.getDefault("DefaultText")
}).width/dpi;
tw = Math.max(tw + this._getArrowSize()*0, this.getDefault("width"));
tw += ts;
tw = ~~tw;
//tw = Math.max(tw, w);
if (this.vertical) {
this.style["width"] = this.dom.style["width"] = this.getDefault("height") + "px";
this.style["height"] = tw + "px";
this.dom.style["height"] = tw + "px";
} else {
this.style["height"] = this.dom.style["height"] = this.getDefault("height") + "px";
this.style["width"] = tw + "px";
this.dom.style["width"] = tw + "px";
}
this._repos_canvas();
this._redraw();
}
updateName(force) {
if (!this.hasAttribute("name")) {
return;
}
let name = this.getAttribute("name");
if (force || name !== this._name) {
this._name = name;
this.setCSS();
}
let label = this._genLabel();
if (label !== this._last_label) {
this._last_label = label;
this.setCSS();
}
}
_genLabel() {
let val = this.value;
let text;
if (val === undefined) {
text = "error";
} else {
val = val === undefined ? 0.0 : val;
if (this.isInt) {
val = Math.floor(val);
}
val = units.buildString(val, this.baseUnit, this.decimalPlaces, this.displayUnit);
text = val;
if (this._name) {
text = this._name + ": " + text;
}
}
return text;
}
_redraw() {
let g = this.g;
let canvas = this.dom;
let dpi = this.getDPI();
let disabled = this.disabled;
let r = this.getDefault("border-radius");
if (this.isInt) {
r *= 0.25;
}
let boxbg = this.getDefault(this._highlight ? "BoxHighlight" : "background-color");
ui_base.drawRoundBox(this, this.dom, this.g, undefined, undefined,
r, "fill", disabled ? this.getDefault("DisabledBG") : boxbg);
ui_base.drawRoundBox(this, this.dom, this.g, undefined, undefined,
r, "stroke", disabled ? this.getDefault("DisabledBG") : this.getDefault("border-color"));
r *= dpi;
let pad = this.getDefault("padding");
let ts = this.getDefault("DefaultText").size;
let text = this._genLabel();
let tw = ui_base.measureText(this, text, this.dom, this.g).width;
let cx = ts + this._getArrowSize();
let cy = this.dom.height/2;
this.dom.font = undefined;
g.save();
let th = Math.PI*0.5;
if (this.vertical) {
g.rotate(th);
ui_base.drawText(this, cx, -ts*0.5, text, {
canvas: this.dom,
g : this.g,
size : ts
});
g.restore();
} else {
ui_base.drawText(this, cx, cy + ts/2, text, {
canvas: this.dom,
g : this.g,
size : ts
});
}
//}
let arrowcolor = this.getDefault("arrow-color") || "33%";
arrowcolor = arrowcolor.trim();
if (arrowcolor.endsWith("%")) {
arrowcolor = arrowcolor.slice(0, arrowcolor.length - 1).trim();
let perc = parseFloat(arrowcolor)/100.0;
let c = css2color(this.getDefault("arrow-color"));
let f = 1.0 - (c[0] + c[1] + c[2])*perc;
f = ~~(f*255);
arrowcolor = `rgba(${f},${f},${f},0.95)`;
}
arrowcolor = css2color(arrowcolor);
let higharrow = css2color(this.getDefault("BoxHighlight"));
higharrow.interp(arrowcolor, 0.5);
arrowcolor = color2css(arrowcolor);
higharrow = color2css(higharrow);
let over = this._highlight ? this.overArrow(this.mpos[0], this.mpos[1]) : 0;
let d = 7, w = canvas.width, h = canvas.height;
let sz = this._getArrowSize();
if (this.vertical) {
g.beginPath();
g.moveTo(w*0.5, d);
g.lineTo(w*0.5 + sz*0.5, d + sz);
g.lineTo(w*0.5 - sz*0.5, d + sz);
g.fillStyle = over < 0 ? higharrow : arrowcolor;
g.fill();
g.beginPath();
g.moveTo(w*0.5, h - d);
g.lineTo(w*0.5 + sz*0.5, h - sz - d);
g.lineTo(w*0.5 - sz*0.5, h - sz - d);
g.fillStyle = over > 0 ? higharrow : arrowcolor;
g.fill();
} else {
g.beginPath();
g.moveTo(d, h*0.5);
g.lineTo(d + sz, h*0.5 + sz*0.5);
g.lineTo(d + sz, h*0.5 - sz*0.5);
g.fillStyle = over < 0 ? higharrow : arrowcolor;
g.fill();
g.beginPath();
g.moveTo(w - d, h*0.5);
g.lineTo(w - sz - d, h*0.5 + sz*0.5);
g.lineTo(w - sz - d, h*0.5 - sz*0.5);
g.fillStyle = over > 0 ? higharrow : arrowcolor;
g.fill();
}
g.fill();
}
_getArrowSize() {
return UIBase.getDPI()*10;
}
}
UIBase.internalRegister(NumSlider);
export class NumSliderSimpleBase extends NumberSliderBase(UIBase) {
constructor() {
super();
this.baseUnit = undefined;
this.displayUnit = undefined;
this.editAsBaseUnit = undefined;
this.canvas = document.createElement("canvas");
this.g = this.canvas.getContext("2d");
this.canvas.style["width"] = this.getDefault("width") + "px";
this.canvas.style["height"] = this.getDefault("height") + "px";
this.canvas.style["pointer-events"] = "none";
this.highlight = false;
this.isInt = false;
this.shadow.appendChild(this.canvas);
this.range = [0, 1];
/** if not undefined defines subrange of visible slider */
this.uiRange = undefined;
this.step = 0.1;
this._value = 0.5;
this.ma = undefined;
this._focus = false;
this.modal = undefined;
this._last_slider_key = '';
}
get value() {
return this._value;
}
set value(val) {
this.setValue(val);
}
static define() {
return {
tagname : "numslider-simple-base-x",
style : "numslider_simple",
parentStyle: "button"
}
}
setValue(val, fire_onchange = true, setDataPath = true) {
val = Math.min(Math.max(val, this.range[0]), this.range[1]);
if (this.isInt) {
val = Math.floor(val);
}
if (this._value !== val) {
this._value = val;
this._redraw();
if (this.onchange && fire_onchange) {
this.onchange(val);
}
if (setDataPath && this.getAttribute("datapath")) {
let path = this.getAttribute("datapath");
this.setPathValue(this.ctx, path, this._value);
}
}
}
updateDataPath() {
if (!this.hasAttribute("datapath")) {
return;
}
let path = this.getAttribute("datapath");
if (!path || path === "null" || path === "undefined") {
return;
}
let val = this.getPathValue(this.ctx, path);
if (this.isInt) {
val = Math.floor(val);
}
if (val !== this._value) {
let prop = this.getPathMeta(this.ctx, path);
if (!prop) {
return;
}
this.loadConstraints(prop);
this.setValue(val, true, false);
}
}
_setFromMouse(e) {
let rect = this.getClientRects()[0];
if (rect === undefined) {
return;
}
let x = e.x - rect.left;
let dpi = UIBase.getDPI();
let co = this._getButtonPos();
let val = this._invertButtonX(x*dpi);
this.value = val;
}
_startModal(e) {
if (this.disabled) {
return;
}
if (e !== undefined) {
this._setFromMouse(e);
}
let dom = window;
let evtargs = {capture: false};
if (this.modal) {
console.warn("Double call to _startModal!");
return;
}
this.ma = new util.MovingAvg(eventWasTouch(e) ? 4 : 2);
let handlers;
let end = () => {
if (handlers === undefined) {
return;
}
this.popModal();
handlers = undefined;
};
handlers = {
mousemove: (e) => {
let x = e.x, y = e.y;
x = this.ma.add(x);
let e2 = new MouseEvent(e, {
x, y
});
this._setFromMouse(e);
},
mouseover : (e) => {
},
mouseout : (e) => {
},
mouseleave: (e) => {
},
mouseenter: (e) => {
},
blur : (e) => {
},
focus : (e) => {
},
mouseup: (e) => {
this.undoBreakPoint();
end();
},
keydown: (e) => {
switch (e.keyCode) {
case keymap["Enter"]:
case keymap["Space"]:
case keymap["Escape"]:
end();
}
}
};
function makefunc(f) {
return (e) => {
e.stopPropagation();
e.preventDefault();
return f(e);
}
}
for (let k in handlers) {
handlers[k] = makefunc(handlers[k]);
}
this.pushModal(handlers);
}
init() {
super.init();
if (!this.hasAttribute("tab-index")) {
this.setAttribute("tab-index", 0);
}
this.updateSize();
this.addEventListener("keydown", (e) => {
let dt = this.range[1] > this.range[0] ? 1 : -1;
switch (e.keyCode) {
case keymap["Left"]:
case keymap["Right"]:
let fac = this.step;
if (e.shiftKey) {
fac *= 0.1;
}
if (this.isInt) {
fac = Math.max(fac, 1);
}
this.value += e.keyCode === keymap["Left"] ? -dt*fac : dt*fac;
break;
}
});
this.addEventListener("focusin", () => {
if (this.disabled) return;
this._focus = 1;
this._redraw();
this.focus();
});
this.addEventListener("mousedown", (e) => {
if (this.disabled) {
return;
}
/* ensure browser doesn't spawn its own (incompatible)
touch->mouse emulation events}; */
e.preventDefault();
this._startModal(e);
});
this.addEventListener("mousein", (e) => {
this.setHighlight(e);
this._redraw();
});
this.addEventListener("mouseout", (e) => {
this.highlight = false;
this._redraw();
});
this.addEventListener("mouseover", (e) => {
this.setHighlight(e);
this._redraw();
});
this.addEventListener("mousemove", (e) => {
this.setHighlight(e);
this._redraw();
});
this.addEventListener("mouseleave", (e) => {
this.highlight = false;
this._redraw();
});
this.addEventListener("blur", (e) => {
this._focus = 0;
this.highlight = false;
this._redraw();
});
this.setCSS();
}
setHighlight(e) {
this.highlight = this.isOverButton(e) ? 2 : 1;
}
_redraw() {
let g = this.g, canvas = this.canvas;
let w = canvas.width, h = canvas.height;
let dpi = UIBase.getDPI();
let color = this.getDefault("background-color");
let sh = ~~(this.getDefault("SlideHeight")*dpi + 0.5);
g.clearRect(0, 0, canvas.width, canvas.height);
g.fillStyle = color;
let y = (h - sh)*0.5;
let r = this.getDefault("border-radius");
g.translate(0, y);
ui_base.drawRoundBox(this, this.canvas, g, w, sh, r, "fill", color, undefined, true);
let bcolor = this.getDefault('border-color');
ui_base.drawRoundBox(this, this.canvas, g, w, sh, r, "stroke", bcolor, undefined, true);
g.translate(0, -y);
if (this.highlight === 1) {
color = this.getDefault("BoxHighlight");
} else {
color = this.getDefault("border-color");
}
g.strokeStyle = color;
g.stroke();
let co = this._getButtonPos();
g.beginPath();
if (this.highlight === 2) {
color = this.getDefault("BoxHighlight");
} else {
color = this.getDefault("border-color");
}
g.arc(co[0], co[1], Math.abs(co[2]), -Math.PI, Math.PI);
g.fill();
g.strokeStyle = color;
g.stroke();
g.beginPath();
g.setLineDash([4, 4]);
if (this._focus) {
g.strokeStyle = this.getDefault("BoxHighlight");
g.arc(co[0], co[1], co[2] - 4, -Math.PI, Math.PI);
g.stroke();
}
g.setLineDash([]);
}
isOverButton(e) {
let x = e.x, y = e.y;
let rect = this.getClientRects()[0];
if (!rect) {
return false;
}
x -= rect.left;
y -= rect.top;
let co = this._getButtonPos();
let dpi = UIBase.getDPI();
let dv = new Vector2([co[0]/dpi - x, co[1]/dpi - y]);
let dis = dv.vectorLength();
return dis < co[2]/dpi;
}
_invertButtonX(x) {
let w = this.canvas.width;
let dpi = UIBase.getDPI();
let sh = ~~(this.getDefault("SlideHeight")*dpi + 0.5);
let boxw = this.canvas.height - 4;
let w2 = w - boxw;
let range = this.uiRange || this.range;
x = (x - boxw*0.5)/w2;
x = x*(range[1] - range[0]) + range[0];
return x;
}
_getButtonPos() {
let w = this.canvas.width;
let dpi = UIBase.getDPI();
let sh = ~~(this.getDefault("SlideHeight")*dpi + 0.5);
let x = this._value;
let range = this.uiRange || this.range;
x = (x - range[0])/(range[1] - range[0]);
let boxw = this.canvas.height - 4;
let w2 = w - boxw;
x = x*w2 + boxw*0.5;
return [x, boxw*0.5, boxw*0.5];
}
setCSS() {
//UIBase.setCSS does annoying thing with background-color
//super.setCSS();
this.canvas.style["width"] = "min-contents";
this.canvas.style["min-width"] = this.getDefault("width") + "px";
this.canvas.style["height"] = this.getDefault("height") + "px";
this.canvas.height = this.getDefault("height")*UIBase.getDPI();
this.style["min-width"] = this.getDefault("width") + "px";
this._redraw();
}
updateSize() {
if (this.canvas === undefined) {
return;
}
let rect = this.getClientRects()[0];
if (rect === undefined) {
return;
}
let dpi = UIBase.getDPI();
let w = ~~(rect.width*dpi), h = ~~(rect.height*dpi);
let canvas = this.canvas;
if (w !== canvas.width || h !== canvas.height) {
this.canvas.width = w;
this.canvas.height = h;
this.setCSS();
this._redraw();
}
}
_ondestroy() {
if (this.modalRunning) {
this.popModal();
}
}
update() {
super.update();
let key = this.getDefault("width") + this.getDefault("height") + this.getDefault("SlideHeight");
if (key !== this._last_slider_key) {
this._last_slider_key = key;
this.flushUpdate();
this.setCSS();
this._redraw();
}
if (this.getAttribute("tab-index") !== this.tabIndex) {
this.tabIndex = this.getAttribute("tab-index");
}
this.updateSize();
this.updateDataPath();
updateSliderFromDom(this);
}
}
UIBase.internalRegister(NumSliderSimpleBase);
export class SliderWithTextbox extends ColumnFrame {
constructor() {
super();
this._value = 0;
this._name = undefined;
this._lock_textbox = false;
this._labelOnTop = undefined;
this._last_label_on_top = undefined;
this.container = this;
this.textbox = UIBase.createElement("textbox-x");
this.textbox.width = 55;
this._numslider = undefined;
this.textbox.overrideDefault("width", this.getDefault("TextBoxWidth"));
this.textbox.setAttribute("class", "numslider_simple_textbox");
this._last_value = undefined;
}
/**
* whether to put label on top or to the left of sliders
*
* If undefined value will be either this.getAtttribute("labelOnTop"),
* if "labelOnTop" attribute exists, or it will be this.getDefault("labelOnTop")
* (theme default)
**/
get labelOnTop() {
let ret = this._labelOnTop;
if (ret === undefined && this.hasAttribute("labelOnTop")) {
let val = this.getAttribute("labelOnTop");
if (typeof val === "string") {
val = val.toLowerCase();
ret = val === "true" || val === "yes";
} else {
ret = !!val;
}
}
if (ret === undefined) {
ret = this.getDefault("labelOnTop");
}
return !!ret;
}
set labelOnTop(v) {
this._labelOnTop = v;
}
get numslider() {
return this._numslider;
}
//child classes set this in their constructors
set numslider(v) {
this._numslider = v;
this.textbox.range = this._numslider.range;
}
get editAsBaseUnit() {
return this.numslider.editAsBaseUnit;
}
set editAsBaseUnit(v) {
this.numslider.editAsBaseUnit = v;
}
get range() {
return this.numslider.range;
}
set range(v) {
this.numslider.range = v;
}
get step() {
return this.numslider.step;
}
set step(v) {
this.numslider.step = v;
}
get expRate() {
return this.numslider.expRate;
}
set expRate(v) {
this.numslider.expRate = v;
}
get decimalPlaces() {
return this.numslider.decimalPlaces;
}
set decimalPlaces(v) {
this.numslider.decimalPlaces = v;
}
get isInt() {
return this.numslider.isInt;
}
set isInt(v) {
this.numslider.isInt = v;
}
get slideSpeed() {
return this.numslider.slideSpeed;
}
set slideSpeed(v) {
this.numslider.slideSpeed = v;
}
get radix() {
return this.numslider.radix;
}
set radix(v) {
this.numslider.radix = v;
}
get stepIsRelative() {
return this.numslider.stepIsRelative;
}
set stepIsRelative(v) {
this.numslider.stepIsRelative = v;
}
get displayUnit() {
return this.numslider.displayUnit;
}
set displayUnit(val) {
let update = val !== this.displayUnit;
this.numslider.displayUnit = this.textbox.displayUnit = val;
if (update) {
//this.numslider._redraw();
this.updateTextBox();
}
}
get baseUnit() {
return this.textbox.baseUnit;
}
set baseUnit(val) {
let update = val !== this.baseUnit;
this.numslider.baseUnit = this.textbox.baseUnit = val;
if (update) {
//this.slider._redraw();
this.updateTextBox();
}
}
get realTimeTextBox() {
let ret = this.getAttribute("realtime");
if (!ret) {
return false;
}
ret = ret.toLowerCase().trim();
return ret === 'true' || ret === 'on' || ret === 'yes';
}
set realTimeTextBox(val) {
this.setAttribute("realtime", val ? "true" : "false");
}
get value() {
return this._value;
}
set value(val) {
this.setValue(val);
}
init() {
super.init();
this.rebuild();
window.setTimeout(() => this.updateTextBox(), 500);
}
rebuild() {
this._last_label_on_top = this.labelOnTop;
this.container.clear();
if (!this.labelOnTop) {
this.container = this.row();
} else {
this.container = this;
}
if (this.hasAttribute("name")) {
this._name = this.hasAttribute("name");
} else {
this._name = "slider";
}
this.l = this.container.label(this._name);
this.l.overrideClass("numslider_textbox");
this.l.font = "TitleText";
this.l.style["display"] = "float";
this.l.style["position"] = "relative";
let strip = this.container.row();
//strip.style['justify-content'] = 'space-between';
strip.add(this.numslider);
let path = this.hasAttribute("datapath") ? this.getAttribute("datapath") : undefined;
let textbox = this.textbox;
this.textbox.overrideDefault("width", this.getDefault("TextBoxWidth"));
let apply_textbox = () => {
let text = textbox.text;
if (!units.isNumber(text)) {
textbox.flash("red");
return;
} else {
textbox.flash("green");
let displayUnit = this.editAsBaseUnit ? undefined : this.displayUnit;
let f = units.parseValue(text, this.baseUnit, displayUnit);
if (isNaN(f)) {
this.flash("red");
return;
}
if (this.isInt) {
f = Math.floor(f);
}
this._lock_textbox = 1;
this.setValue(f);
this._lock_textbox = 0;
}
};
if (this.realTimeTextBox) {
textbox.onchange = apply_textbox;
}
textbox.onend = apply_textbox;
textbox.ctx = this.ctx;
textbox.packflag |= this.inherit_packflag;
textbox.overrideDefault("width", this.getDefault("TextBoxWidth"));
textbox.style["height"] = (this.getDefault("height") - 2) + "px";
textbox._init();
strip.add(textbox);
textbox.setCSS();
this.linkTextBox();
let in_onchange = 0;
this.numslider.onchange = (val) => {
this._value = this.numslider.value;
this.updateTextBox();
if (in_onchange) {
return;
}
if (this.onchange !== undefined) {
in_onchange++;
try {
if (this.onchange) {
this.onchange(this);
}
} catch (error) {
util.print_stack(error);
}
}
in_onchange--;
}
}
updateTextBox() {
if (!this._init_done) {
return;
}
if (this._lock_textbox > 0)
return;
this.textbox.text = this.formatNumber(this._value);
this.textbox.update();
updateSliderFromDom(this, this.numslider);
}
linkTextBox() {
this.updateTextBox();
let onchange = this.numslider.onchange;
this.numslider.onchange = (e) => {
this._value = e.value;
this.updateTextBox();
onchange(e);
}
}
setValue(val, fire_onchange = true) {
this._value = val;
this.numslider.setValue(val, fire_onchange);
this.updateTextBox();
}
updateName() {
let name = this.getAttribute("name");
if (!name && this.hasAttribute("datapath")) {
let prop = this.getPathMeta(this.ctx, this.getAttribute("datapath"));
if (prop) {
name = prop.uiname;
}
}
if (!name) {
name = "[error]";
}
if (name !== this._name) {
this._name = name;
this.l.text = name;
}
}
updateLabelOnTop() {
if (this.labelOnTop !== this._last_label_on_top) {
this._last_label_on_top = this.labelOnTop;
this.rebuild();
}
}
updateDataPath() {
if (!this.ctx || !this.getAttribute("datapath")) {
return;
}
let prop = this.getPathMeta(this.ctx, this.getAttribute("datapath"))
if (!prop) {
return;
}
let val = this.getPathValue(this.ctx, this.getAttribute("datapath"));
if (val !== this._last_value) {
this._last_value = this._value = val;
this.updateTextBox();
}
}
update() {
this.updateLabelOnTop();
super.update();
this.updateDataPath();
let redraw = false;
updateSliderFromDom(this.numslider, this);
updateSliderFromDom(this.textbox, this);
if (redraw) {
this.setCSS();
this.numslider.setCSS();
this.numslider._redraw();
}
this.updateName();
this.numslider.description = this.description;
this.textbox.description = this.title; //get full, transformed toolip
if (this.hasAttribute("datapath")) {
this.numslider.setAttribute("datapath", this.getAttribute("datapath"));
}
if (this.hasAttribute("mass_set_path")) {
this.numslider.setAttribute("mass_set_path", this.getAttribute("mass_set_path"))
}
}
setCSS() {
super.setCSS();
this.textbox.setCSS();
//textbox.style["margin"] = "5px";
}
}
export class NumSliderSimple extends SliderWithTextbox {
constructor() {
super();
this.numslider = UIBase.createElement("numslider-simple-base-x");
}
static define() {
return {
tagname: "numslider-simple-x",
style : "numslider_simple"
}
}
_redraw() {
this.numslider._redraw();
}
}
UIBase.internalRegister(NumSliderSimple);
export class NumSliderWithTextBox extends SliderWithTextbox {
constructor() {
super();
this.numslider = UIBase.createElement("numslider-x");
}
static define() {
return {
tagname: "numslider-textbox-x",
style : "numslider_textbox"
}
}
_redraw() {
this.numslider._redraw();
}
}
UIBase.internalRegister(NumSliderWithTextBox);