IDE: Fix PLCOpenEditor (wxPython4 menus) and drop broken PDF doc support.
// widgets_common.ysl2
in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
with "hmi_element", "$hmi_element";
with "labels"{text *ptr};
content;
};
decl optional_labels(*ptr) alias - {
/* TODO add some per label xslt variable to check if exist */
labels(*ptr){
with "mandatory","'no'";
content;
}
};
decl warning_labels(*ptr) alias - {
labels(*ptr){
with "mandatory","'warn'";
content;
}
};
decl _activable(*level) alias - {
| activable_sub:{
const "activity" labels("/active /inactive") {
with "mandatory"{text *level};
content;
}
value "$activity";
const "has_activity","string-length($activity)>0";
| },
| has_activity: «$has_activity»,
};
decl activable() alias - {
_activable("warn")
};
decl optional_activable() alias - {
_activable("no")
};
decl activable_labels(*ptr) alias - {
optional_labels(*ptr) {
with "subelements","'active inactive'";
content;
}
};
in xsl decl widget_desc(%name, match="widget[@type='%name']", mode="widget_desc") alias template {
type > «@type»
content;
};
in xsl decl widget_class(%name, *clsname="%nameWidget", match="widget[@type='%name']", mode="widget_class") alias template {
| class `text **clsname` extends Widget{
content;
| }
};
in xsl decl widget_defs(%name, match="widget[@type='%name']", mode="widget_defs") alias template {
param "hmi_element";
// all widget potentially has a "disabled" labeled element
const "disability" optional_labels("/disabled");
value "$disability";
const "has_disability","string-length($disability)>0";
content;
};
in xsl decl widget_page(%name, match="widget[@type='%name']", mode="widget_page") alias template {
param "page_desc";
content;
};
decl gen_index_xhtml alias - {
content;
};
template "svg:*", mode="hmi_widgets" {
const "widget", "func:widget(@id)";
const "eltid","@id";
const "args" foreach "$widget/arg" > "«func:escape_quotes(@value)»"`if "position()!=last()" > ,`
const "indexes" foreach "$widget/path" {
if "position()!=last()" > ,
}
const "variables" foreach "$widget/path" {
> [
choose {
when "not(@index)" {
choose {
when "not(@type)" {
warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
> undefined
}
when "@type = 'PAGE_LOCAL'"
> "«@value»"
when "@type = 'HMI_LOCAL'"
> hmi_local_index("«@value»")
otherwise
error > Internal error while processing widget's non indexed HMI tree path : unknown type
}
}
otherwise {
> «@index»
}
}
> , {
if "@min and @max"{
> minmax:[«@min», «@max»]
if "@assign"
> ,
}
if "@assign"
> assign:"«@assign»"
> }]
if "position()!=last()" > ,
}
const "freq" choose {
when "$widget/@freq"
> "«$widget/@freq»"
otherwise
> undefined
}
const "enable_expr" choose{
when "$widget/@enable_expr"
> true
otherwise
> false
}
| "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$variables»],«$enable_expr»,{
if "$widget/@enable_expr" {
| enable_assignments: [],
| compute_enable: function(value, oldval, varnum) {
| let result = false;
| do {
foreach "$widget/path" {
const "varid","generate-id()";
const "varnum","position()-1";
if "@assign" foreach "$widget/path[@assign]" if "$varid = generate-id()" {
| if(varnum == «$varnum») this.enable_assignments[«position()-1»] = value;
| let «@assign» = this.enable_assignments[«position()-1»];
| if(«@assign» == undefined) break;
}
}
| result = «$widget/@enable_expr»;
| } while(0);
| this.enable(result);
| },
}
apply "$widget", mode="widget_defs" with "hmi_element",".";
| })`if "position()!=last()" > ,`
}
emit "preamble:local-variable-indexes" {
||
let hmi_locals = {};
var last_remote_index = hmitree_types.length - 1;
var next_available_index = hmitree_types.length;
let cookies = new Map(document.cookie.split("; ").map(s=>s.split("=")));
const local_defaults = {
||
foreach "$parsed_widgets/widget[starts-with(@type,'VarInit')]"{
if "count(path) != 1" error > VarInit «@id» must have only one variable given.
if "path/@type != 'PAGE_LOCAL' and path/@type != 'HMI_LOCAL'" error > VarInit «@id» only applies to HMI variable.
> "«path/@value»":
choose {
when "@type = 'VarInitPersistent'" > cookies.has("«path/@value»")?cookies.get("«path/@value»"):«arg[1]/@value»
otherwise > «arg[1]/@value»
}
> \n
if "position()!=last()" > ,
}
||
};
const persistent_locals = new Set([
||
foreach "$parsed_widgets/widget[@type='VarInitPersistent']"{
| "«path/@value»"`if "position()!=last()" > ,`
}
||
]);
var persistent_indexes = new Map();
var cache = hmitree_types.map(_ignored => undefined);
function page_local_index(varname, pagename){
let pagevars = hmi_locals[pagename];
let new_index;
if(pagevars == undefined){
new_index = next_available_index++;
hmi_locals[pagename] = {[varname]:new_index};
} else {
let result = pagevars[varname];
if(result != undefined) {
return result;
}
new_index = next_available_index++;
pagevars[varname] = new_index;
}
let defaultval = local_defaults[varname];
if(defaultval != undefined) {
cache[new_index] = defaultval;
if(persistent_locals.has(varname))
persistent_indexes.set(new_index, varname);
}
return new_index;
}
function hmi_local_index(varname){
return page_local_index(varname, "HMI_LOCAL");
}
||
}
emit "preamble:widget-base-class" {
||
var pending_widget_animates = [];
function _hide(elt, placeholder){
if(elt.parentNode != null)
placeholder.parentNode.removeChild(elt);
}
function _show(elt, placeholder){
placeholder.parentNode.insertBefore(elt, placeholder);
}
function set_activity_state(eltsub, state){
if(eltsub.active_elt != undefined){
if(eltsub.active_elt_placeholder == undefined){
eltsub.active_elt_placeholder = document.createComment("");
eltsub.active_elt.parentNode.insertBefore(eltsub.active_elt_placeholder, eltsub.active_elt);
}
(state?_show:_hide)(eltsub.active_elt, eltsub.active_elt_placeholder);
}
if(eltsub.inactive_elt != undefined){
if(eltsub.inactive_elt_placeholder == undefined){
eltsub.inactive_elt_placeholder = document.createComment("");
eltsub.inactive_elt.parentNode.insertBefore(eltsub.inactive_elt_placeholder, eltsub.inactive_elt);
}
((state || state==undefined)?_hide:_show)(eltsub.inactive_elt, eltsub.inactive_elt_placeholder);
}
}
class Widget {
offset = 0;
frequency = 10; /* FIXME arbitrary default max freq. Obtain from config ? */
unsubscribable = false;
pending_animate = false;
constructor(elt_id, freq, args, variables, enable_expr, members){
this.element_id = elt_id;
this.element = id(elt_id);
this.args = args;
[this.indexes, this.variables_options] = (variables.length>0) ? zip(...variables) : [[],[]];
this.indexes_length = this.indexes.length;
this.enable_expr = enable_expr;
this.enable_state = true;
this.enable_displayed_state = true;
this.enabled_elts = [];
Object.keys(members).forEach(prop => this[prop]=members[prop]);
this.lastapply = this.indexes.map(() => undefined);
this.inhibit = this.indexes.map(() => undefined);
this.pending = this.indexes.map(() => undefined);
this.bound_uninhibit = this.uninhibit.bind(this);
this.lastdispatch = this.indexes.map(() => undefined);
this.deafen = this.indexes.map(() => undefined);
this.incoming = this.indexes.map(() => undefined);
this.bound_undeafen = this.undeafen.bind(this);
this.forced_frequency = freq;
this.clip = true;
}
do_init(){
let forced = this.forced_frequency;
if(forced !== undefined){
/*
once every 10 seconds : 10s
once per minute : 1m
once per hour : 1h
once per day : 1d
*/
let unit = forced.slice(-1);
let factor = {
"s":1,
"m":60,
"h":3600,
"d":86400}[unit];
this.frequency = factor ? 1/(factor * Number(forced.slice(0,-1)))
: Number(forced);
}
let init = this.init;
if(typeof(init) == "function"){
try {
init.call(this);
} catch(err) {
console.log(err);
}
}
if(this.enable_expr){
this.enable_state = false;
this.enable_displayed_state = false;
for(let child of Array.from(this.element.children)){
let label = child.getAttribute("inkscape:label");
if(label!="disabled"){
this.enabled_elts.push(child);
this.element.removeChild(child);
}
}
}
}
unsub(){
/* remove subsribers */
for(let i = 0; i < this.indexes_length; i++) {
/* flush updates pending because of inhibition */
let inhibition = this.inhibit[i];
if(inhibition != undefined){
clearTimeout(inhibition);
this.lastapply[i] = undefined;
this.uninhibit(i);
}
let deafened = this.deafen[i];
if(deafened != undefined){
clearTimeout(deafened);
this.lastdispatch[i] = undefined;
this.undeafen(i);
}
let index = this.get_variable_index(i);
subscribers(index).delete(this);
}
this.offset = 0;
this.relativeness = undefined;
}
sub(new_offset, relativeness, container_id){
this.offset = new_offset;
this.relativeness = relativeness;
this.container_id = container_id ;
/* add this's subsribers */
for(let i = 0; i < this.indexes_length; i++) {
let index = this.get_variable_index(i);
if(index == undefined) continue;
subscribers(index).add(this);
}
this.apply_cache();
}
apply_cache() {
for(let i = 0; i < this.indexes_length; i++) {
/* dispatch current cache in newly opened page widgets */
let realindex = this.get_variable_index(i);
if(realindex == undefined) continue;
let cached_val = cache[realindex];
if(cached_val != undefined)
this.feed_data_for_dispatch(cached_val, cached_val, i);
}
}
get_variable_index(varnum) {
let index = this.indexes[varnum];
if(typeof(index) == "string"){
index = page_local_index(index, this.container_id);
} else {
if(this.relativeness[varnum]){
index += this.offset;
}
}
return index;
}
overshot(new_val, max) {
}
undershot(new_val, min) {
}
clip_min_max(index, new_val) {
let minmax = this.variables_options[index].minmax;
if(minmax !== undefined && typeof new_val == "number") {
let [min,max] = minmax;
if(new_val < min){
this.undershot(new_val, min);
return min;
}
if(new_val > max){
this.overshot(new_val, max);
return max;
}
}
return new_val;
}
change_hmi_value(index, opstr) {
let realindex = this.get_variable_index(index);
if(realindex == undefined) return undefined;
let old_val = cache[realindex];
let new_val = eval_operation_string(old_val, opstr);
if(this.clip)
new_val = this.clip_min_max(index, new_val);
return apply_hmi_value(realindex, new_val);
}
_apply_hmi_value(index, new_val) {
let realindex = this.get_variable_index(index);
if(realindex == undefined) return undefined;
if(this.clip)
new_val = this.clip_min_max(index, new_val);
return apply_hmi_value(realindex, new_val);
}
uninhibit(index){
this.inhibit[index] = undefined;
let new_val = this.pending[index];
this.pending[index] = undefined;
return this.apply_hmi_value(index, new_val);
}
apply_hmi_value(index, new_val) {
if(this.inhibit[index] == undefined){
let now = Date.now();
let min_interval = 1000/this.frequency;
let lastapply = this.lastapply[index];
if(lastapply == undefined || now > lastapply + min_interval){
this.lastapply[index] = now;
return this._apply_hmi_value(index, new_val);
}
else {
let elapsed = now - lastapply;
this.pending[index] = new_val;
this.inhibit[index] = setTimeout(this.bound_uninhibit, min_interval - elapsed, index);
}
}
else {
this.pending[index] = new_val;
return new_val;
}
}
new_hmi_value(index, value, oldval) {
// TODO avoid searching, store index at sub()
for(let i = 0; i < this.indexes_length; i++) {
let refindex = this.get_variable_index(i);
if(refindex == undefined) continue;
if(index == refindex) {
this.feed_data_for_dispatch(value, oldval, i);
break;
}
}
}
undeafen(index){
this.deafen[index] = undefined;
let [new_val, old_val] = this.incoming[index];
this.incoming[index] = undefined;
this.lastdispatch[index] = Date.now();
this.do_dispatch(new_val, old_val, index);
}
enable(enabled){
if(this.enable_state != enabled){
this.enable_state = enabled;
this.request_animate();
}
}
animate_enable(){
if(this.enable_state && !this.enable_displayed_state){
//show widget
for(let child of this.enabled_elts){
this.element.appendChild(child);
}
//hide disabled content
if(this.disabled_elt && this.disabled_elt.parentNode != null)
this.element.removeChild(this.disabled_elt);
this.enable_displayed_state = true;
}else if(!this.enable_state && this.enable_displayed_state){
//hide widget
for(let child of this.enabled_elts){
if(child.parentNode != null)
this.element.removeChild(child);
}
//show disabled content
if(this.disabled_elt)
this.element.appendChild(this.disabled_elt);
this.enable_displayed_state = false;
// once disabled activity display is lost
this.activity_displayed_state = undefined;
}
}
feed_data_for_dispatch(value, oldval, varnum) {
if(this.dispatch || this.enable_expr){
if(this.deafen[varnum] == undefined){
let now = Date.now();
let min_interval = 1000/this.frequency;
let lastdispatch = this.lastdispatch[varnum];
if(lastdispatch == undefined || now > lastdispatch + min_interval){
this.lastdispatch[varnum] = now;
this.do_dispatch(value, oldval, varnum)
}
else {
let elapsed = now - lastdispatch;
this.incoming[varnum] = [value, oldval];
this.deafen[varnum] = setTimeout(this.bound_undeafen, min_interval - elapsed, varnum);
}
}
else {
this.incoming[varnum] = [value, oldval];
}
}
}
do_dispatch(value, oldval, varnum) {
if(this.dispatch) try {
this.dispatch(value, oldval, varnum);
} catch(err) {
console.log(err);
}
if(this.enable_expr) try {
this.compute_enable(value, oldval, varnum);
} catch(err) {
console.log(err);
}
}
_animate(){
if(this.enable_expr)
this.animate_enable();
// inhibit widget animation when disabled
if(!this.enable_expr || this.enable_state){
if(this.has_activity)
this.animate_activity();
if(this.animate != undefined)
this.animate();
}
this.pending_animate = false;
}
request_animate(){
if(!this.pending_animate){
pending_widget_animates.push(this);
this.pending_animate = true;
requestHMIAnimation();
}
}
animate_activity(){
if(this.activity_displayed_state != this.activity_state){
set_activity_state(this.activable_sub, this.activity_state);
this.activity_displayed_state = this.activity_state;
}
}
}
||
}
const "excluded_types", "str:split('Page VarInit VarInitPersistent')";
// Key to filter unique types
key "TypesKey", "widget", "@type";
emit "declarations:hmi-classes" {
const "used_widget_types", """$parsed_widgets/widget[
generate-id() = generate-id(key('TypesKey', @type)) and
not(@type = $excluded_types)]""";
apply "$used_widget_types", mode="widget_class";
}
template "widget", mode="widget_class" {
||
class «@type»Widget extends Widget{
/* empty class, as «@type» widget didn't provide any */
}
||
warning > «@type» widget is used in SVG but widget type is not declared
}
const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id";
const "page_ids","$parsed_widgets/widget[@type = 'Page']/@id";
const "hmi_widgets","$hmi_elements[@id = $included_ids]";
const "page_widgets","$hmi_elements[@id = $page_ids]";
const "result_widgets","$result_svg_ns//*[@id = $hmi_widgets/@id]";
emit "declarations:hmi-elements" {
| var hmi_widgets = {
apply "$hmi_widgets | $page_widgets", mode="hmi_widgets";
| }
|
}
function "defs_by_labels" {
param "labels","''";
param "mandatory","'yes'";
param "subelements","/..";
param "hmi_element";
const "widget_type","@type";
const "widget_id","@id";
foreach "str:split($labels)" {
const "absolute", "starts-with(., '/')";
const "name","substring(.,number($absolute)+1)";
const "widget","$result_widgets[@id = $hmi_element/@id]";
const "elt","($widget//*[not($absolute) and @inkscape:label=$name] | $widget/*[$absolute and @inkscape:label=$name])[1]";
choose {
when "not($elt/@id)" {
if "$mandatory!='no'" {
const "errmsg" > «$widget_type» widget (id=«$widget_id») must have a «$name» element
choose {
when "$mandatory='yes'" {
error > «$errmsg»
}
otherwise {
warning > «$errmsg»
}
}
}
// otherwise produce nothing
}
otherwise {
| «$name»_elt: id("«$elt/@id»"),
if "$subelements" {
| «$name»_sub: {
foreach "str:split($subelements)" {
const "subname",".";
const "subelt","$elt/*[@inkscape:label=$subname][1]";
choose {
when "not($subelt/@id)" {
if "$mandatory!='no'" {
const "errmsg" > «$widget_type» widget (id=«$widget_id») must have a «$name»/«$subname» element
choose {
when "$mandatory='yes'" {
error > «$errmsg»
}
otherwise {
warning > «$errmsg»
}
}
}
| /* missing «$name»/«$subname» element */
}
otherwise {
| "«$subname»_elt": id("«$subelt/@id»")`if "position()!=last()" > ,`
}
}
}
| },
}
}
}
}
}
def "func:escape_quotes" {
param "txt";
// have to use a python string to enter escaped quote
// const "frstln", "string-length($frst)";
choose {
when !"contains($txt,'\"')"! {
result !"concat(substring-before($txt,'\"'),'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
}
otherwise {
result "$txt";
}
}
}