edouard@2884: // widgets_common.ysl2
edouard@2884: 
Edouard@2808: in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
Edouard@2808:     with "hmi_element", "$hmi_element";
Edouard@2810:     with "labels"{text *ptr};
edouard@2937:     content;
Edouard@2808: };
Edouard@2808: 
edouard@2937: decl optional_labels(*ptr) alias - {
edouard@2937:     /* TODO add some per label xslt variable to check if exist */
edouard@2937:     labels(*ptr){
edouard@2937:         with "mandatory","'no'";
edouard@2937:         content;
edouard@2937:     }
Edouard@2836: };
Edouard@2836: 
edouard@2937: decl activable_labels(*ptr) alias - {
edouard@2937:     optional_labels(*ptr) {
edouard@2937:         with "subelements","'active inactive'";
edouard@2937:         content;
edouard@2937:     }
edouard@2920: };
edouard@2920: 
edouard@3233: in xsl decl widget_desc(%name, match="widget[@type='%name']", mode="widget_desc") alias template {
edouard@3233:     type > «@type»
edouard@3233:     content;
edouard@3233: };
edouard@3233: 
edouard@3232: in xsl decl widget_class(%name, *clsname="%nameWidget", match="widget[@type='%name']", mode="widget_class") alias template {
edouard@3232:     | class `text **clsname` extends Widget{
edouard@3232:     content;
edouard@3232:     | }
edouard@3232: };
edouard@3232: 
edouard@3232: in xsl decl widget_defs(%name, match="widget[@type='%name']", mode="widget_defs") alias template {
edouard@3232:     param "hmi_element";
edouard@3232:     content;
edouard@3232: };
edouard@3232: 
edouard@3232: in xsl decl widget_page(%name, match="widget[@type='%name']", mode="widget_page") alias template {
edouard@3232:     param "page_desc";
edouard@3232:     content;
edouard@3232: };
edouard@3232: 
edouard@3232: decl gen_index_xhtml alias - {
edouard@3232:     content;
edouard@3232: };
edouard@3232: 
edouard@2955: template "svg:*", mode="hmi_widgets" {
edouard@2886:     const "widget", "func:widget(@id)";
Edouard@2881:     const "eltid","@id";
Edouard@3024:     const "args" foreach "$widget/arg" > "«func:escape_quotes(@value)»"`if "position()!=last()" > ,`
edouard@2950:     const "indexes" foreach "$widget/path" {
Edouard@2881:         choose {
Edouard@2881:             when "not(@index)" {
edouard@3017:                 choose {
Edouard@3058:                     when "not(@type)" {
usveticic@3056:                         warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
edouard@3101:                         > undefined
Edouard@3058:                     }
edouard@3017:                     when "@type = 'PAGE_LOCAL'" 
edouard@3101:                         > "«@value»"
edouard@3017:                     when "@type = 'HMI_LOCAL'" 
edouard@3101:                         > hmi_local_index("«@value»")
edouard@3101:                     otherwise 
edouard@3101:                         error > Internal error while processing widget's non indexed HMI tree path : unknown type
edouard@3017:                 }
Edouard@2881:             }
Edouard@2881:             otherwise {
edouard@3101:                 > «@index»
edouard@3101:             }
edouard@3101:         }
edouard@3101:         if "position()!=last()" > ,
Edouard@2792:     }
edouard@2950: 
edouard@3099:     const "minmaxes" foreach "$widget/path" {
edouard@3099:         choose {
edouard@3099:             when "@min and @max"
edouard@3099:                 > [«@min»,«@max»]
edouard@3099:             otherwise
edouard@3099:                 > undefined
edouard@3099:         }
edouard@3099:         if "position()!=last()" > ,
edouard@3099:     }
edouard@3099: 
edouard@3099:     |   "«@id»": new «$widget/@type»Widget ("«@id»",[«$args»],[«$indexes»],[«$minmaxes»],{
Edouard@2881:     apply "$widget", mode="widget_defs" with "hmi_element",".";
edouard@2949:     |   })`if "position()!=last()" > ,`
edouard@2949: }
edouard@2948: 
edouard@3017: emit "preamble:local-variable-indexes" {
edouard@3017:     ||
edouard@3142: 
edouard@3017:     let hmi_locals = {};
edouard@3017:     var last_remote_index = hmitree_types.length - 1;
edouard@3017:     var next_available_index = hmitree_types.length;
edouard@3128:     let cookies = new Map(document.cookie.split("; ").map(s=>s.split("=")));
edouard@3017: 
Edouard@3025:     const local_defaults = {
Edouard@3025:     ||
edouard@3128:     foreach "$parsed_widgets/widget[starts-with(@type,'VarInit')]"{
Edouard@3025:         if "count(path) != 1" error > VarInit «@id» must have only one variable given.
Edouard@3025:         if "path/@type != 'PAGE_LOCAL' and path/@type != 'HMI_LOCAL'" error > VarInit «@id» only applies to HMI variable.
edouard@3128:         >     "«path/@value»":
edouard@3128:         choose {
edouard@3128:             when "@type = 'VarInitPersistent'" > cookies.has("«path/@value»")?cookies.get("«path/@value»"):«arg[1]/@value»
edouard@3128:             otherwise > «arg[1]/@value»
edouard@3128:         }
edouard@3128:         > \n
edouard@3128:         if "position()!=last()" > ,
Edouard@3025:     }
Edouard@3025:     ||
Edouard@3025:     };
edouard@3128: 
edouard@3128:     const persistent_locals = new Set([
edouard@3128:     ||
edouard@3128:     foreach "$parsed_widgets/widget[@type='VarInitPersistent']"{
edouard@3128:     |    "«path/@value»"`if "position()!=last()" > ,`
edouard@3128:     }
edouard@3128:     ||
edouard@3128:     ]);
edouard@3128:     var persistent_indexes = new Map();
Edouard@3022:     var cache = hmitree_types.map(_ignored => undefined);
edouard@3152:     var updates = new Map();
Edouard@3022: 
edouard@3017:     function page_local_index(varname, pagename){
edouard@3017:         let pagevars = hmi_locals[pagename];
Edouard@3022:         let new_index;
edouard@3017:         if(pagevars == undefined){
Edouard@3022:             new_index = next_available_index++;
Edouard@3022:             hmi_locals[pagename] = {[varname]:new_index}
edouard@3017:         } else {
edouard@3017:             let result = pagevars[varname];
Edouard@3022:             if(result != undefined) {
Edouard@3022:                 return result;
Edouard@3022:             }
Edouard@3022: 
Edouard@3022:             new_index = next_available_index++;
Edouard@3022:             pagevars[varname] = new_index;
Edouard@3022:         }
Edouard@3025:         let defaultval = local_defaults[varname];
edouard@3128:         if(defaultval != undefined) {
Edouard@3025:             cache[new_index] = defaultval; 
edouard@3152:             updates.set(new_index, defaultval);
edouard@3128:             if(persistent_locals.has(varname))
edouard@3128:                 persistent_indexes.set(new_index, varname);
edouard@3128:         }
Edouard@3022:         return new_index;
edouard@3017:     }
edouard@3017: 
edouard@3017:     function hmi_local_index(varname){
edouard@3017:         return page_local_index(varname, "HMI_LOCAL");
edouard@3017:     }
edouard@3017:     ||
edouard@3017: }
edouard@3017: 
edouard@2949: emit "preamble:widget-base-class" {
edouard@3017:     ||
Edouard@3019:     var pending_widget_animates = [];
Edouard@3019: 
edouard@2949:     class Widget {
edouard@2963:         offset = 0;
edouard@2963:         frequency = 10; /* FIXME arbitrary default max freq. Obtain from config ? */
Edouard@2980:         unsubscribable = false;
Edouard@3019:         pending_animate = false;
Edouard@3019: 
edouard@3099:         constructor(elt_id,args,indexes,minmaxes,members){
edouard@2958:             this.element_id = elt_id;
edouard@2950:             this.element = id(elt_id);
edouard@2950:             this.args = args;
edouard@2950:             this.indexes = indexes;
edouard@3099:             this.minmaxes = minmaxes;
edouard@2949:             Object.keys(members).forEach(prop => this[prop]=members[prop]);
edouard@3139:             this.lastapply = indexes.map(() => undefined);
edouard@3139:             this.inhibit = indexes.map(() => undefined);
edouard@3139:             this.pending = indexes.map(() => undefined);
edouard@3139:             this.bound_unhinibit = this.unhinibit.bind(this);
edouard@2949:         }
edouard@2958: 
edouard@2951:         unsub(){
edouard@2951:             /* remove subsribers */
edouard@3005:             if(!this.unsubscribable)
edouard@3005:                 for(let i = 0; i < this.indexes.length; i++) {
edouard@3139:                     /* flush updates pending because of inhibition */
edouard@3142:                     let inhibition = this.inhibit[i];
edouard@3139:                     if(inhibition != undefined){
edouard@3139:                         clearTimeout(inhibition);
edouard@3142:                         this.lastapply[i] = undefined;
edouard@3142:                         this.unhinibit(i);
edouard@3139:                     }
edouard@3005:                     let index = this.indexes[i];
edouard@3005:                     if(this.relativeness[i])
edouard@3005:                         index += this.offset;
Edouard@3022:                     subscribers(index).delete(this);
edouard@3005:                 }
edouard@2951:             this.offset = 0;
edouard@3005:             this.relativeness = undefined;
edouard@3005:         }
edouard@3005: 
edouard@3017:         sub(new_offset=0, relativeness, container_id){
edouard@2951:             this.offset = new_offset;
edouard@3005:             this.relativeness = relativeness;
edouard@3017:             this.container_id = container_id ;
edouard@2951:             /* add this's subsribers */
edouard@3005:             if(!this.unsubscribable)
edouard@3005:                 for(let i = 0; i < this.indexes.length; i++) {
edouard@3017:                     let index = this.get_variable_index(i);
Edouard@3058:                     if(index == undefined) continue;
Edouard@3022:                     subscribers(index).add(this);
edouard@3005:                 }
edouard@2951:             need_cache_apply.push(this); 
edouard@2951:         }
edouard@2951: 
edouard@2951:         apply_cache() {
Edouard@3025:             if(!this.unsubscribable) for(let index in this.indexes){
edouard@2951:                 /* dispatch current cache in newly opened page widgets */
Edouard@3024:                 let realindex = this.get_variable_index(index);
Edouard@3058:                 if(realindex == undefined) continue;
edouard@2951:                 let cached_val = cache[realindex];
edouard@2951:                 if(cached_val != undefined)
Edouard@3026:                     this._dispatch(cached_val, cached_val, index);
edouard@2951:             }
edouard@2951:         }
edouard@2951: 
edouard@3017:         get_variable_index(varnum) {
edouard@3017:             let index = this.indexes[varnum];
edouard@3017:             if(typeof(index) == "string"){
edouard@3017:                 index = page_local_index(index, this.container_id);
edouard@3017:             } else {
edouard@3017:                 if(this.relativeness[varnum]){
edouard@3017:                     index += this.offset;
edouard@3017:                 }
edouard@3017:             }
edouard@3017:             return index;
edouard@3001:         }
edouard@3098: 
edouard@3099:         overshot(new_val, max) {
edouard@3099:         }
edouard@3099: 
edouard@3099:         undershot(new_val, min) {
edouard@3099:         }
edouard@3099: 
edouard@3099:         clip_min_max(index, new_val) {
edouard@3099:             let minmax = this.minmaxes[index];
edouard@3099:             if(minmax !== undefined && typeof new_val == "number") {
edouard@3099:                 let [min,max] = minmax;
edouard@3099:                 if(new_val < min){
edouard@3099:                     this.undershot(new_val, min);
edouard@3099:                     return min;
edouard@3099:                 }
edouard@3099:                 if(new_val > max){
edouard@3099:                     this.overshot(new_val, max);
edouard@3099:                     return max;
edouard@3099:                 }
edouard@3099:             }
edouard@3099:             return new_val;
edouard@3099:         }
edouard@3099: 
Edouard@3058:         change_hmi_value(index, opstr) {
Edouard@3058:             let realindex = this.get_variable_index(index);
Edouard@3058:             if(realindex == undefined) return undefined;
edouard@3098:             let old_val = cache[realindex];
edouard@3098:             let new_val = eval_operation_string(old_val, opstr);
edouard@3099:             new_val = this.clip_min_max(index, new_val);
edouard@3098:             return apply_hmi_value(realindex, new_val);
edouard@3004:         }
edouard@3004: 
edouard@3139:         _apply_hmi_value(index, new_val) {
Edouard@3058:             let realindex = this.get_variable_index(index);
Edouard@3058:             if(realindex == undefined) return undefined;
edouard@3099:             new_val = this.clip_min_max(index, new_val);
Edouard@3058:             return apply_hmi_value(realindex, new_val);
edouard@3004:         }
edouard@3006: 
edouard@3139:         unhinibit(index){
edouard@3139:             this.inhibit[index] = undefined;
edouard@3139:             let new_val = this.pending[index];
edouard@3139:             this.pending[index] = undefined;
edouard@3139:             return this.apply_hmi_value(index, new_val);
edouard@3139:         }
edouard@3139: 
edouard@3139:         apply_hmi_value(index, new_val) {
edouard@3139:             if(this.inhibit[index] == undefined){
edouard@3139:                 let now = Date.now();
edouard@3139:                 let min_interval = 1000/this.frequency;
edouard@3139:                 let lastapply = this.lastapply[index];
edouard@3139:                 if(lastapply == undefined || now > lastapply + min_interval){
edouard@3139:                     this.lastapply[index] = now;
edouard@3139:                     return this._apply_hmi_value(index, new_val);
edouard@3139:                 }
edouard@3139:                 else {
edouard@3139:                     let elapsed = now - lastapply;
edouard@3139:                     this.pending[index] = new_val;
edouard@3139:                     this.inhibit[index] = setTimeout(this.bound_unhinibit, min_interval - elapsed, index);
edouard@3139:                 }
edouard@3139:             }
edouard@3139:             else {
edouard@3139:                 this.pending[index] = new_val;
edouard@3139:                 return new_val;
edouard@3139:             }
edouard@3139:         }
edouard@3139: 
edouard@3006:         new_hmi_value(index, value, oldval) {
Edouard@3025:             // TODO avoid searching, store index at sub()
Edouard@3025:             for(let i = 0; i < this.indexes.length; i++) {
Edouard@3025:                 let refindex = this.get_variable_index(i);
Edouard@3058:                 if(refindex == undefined) continue;
Edouard@3025: 
Edouard@3025:                 if(index == refindex) {
Edouard@3026:                     this._dispatch(value, oldval, i);
Edouard@3025:                     break;
Edouard@3025:                 }
edouard@3006:             }
edouard@3006:         }
Edouard@3019:         
Edouard@3026:         _dispatch(value, oldval, varnum) {
Edouard@3026:             let dispatch = this.dispatch;
Edouard@3026:             if(dispatch != undefined){
Edouard@3026:                 try {
Edouard@3026:                     dispatch.call(this, value, oldval, varnum);
Edouard@3026:                 } catch(err) {
Edouard@3026:                     console.log(err);
Edouard@3026:                 }
Edouard@3026:             }
Edouard@3026:         }
Edouard@3026: 
Edouard@3019:         _animate(){
Edouard@3019:             this.animate();
Edouard@3019:             this.pending_animate = false;
Edouard@3019:         }
Edouard@3019: 
Edouard@3019:         request_animate(){
Edouard@3019:             if(!this.pending_animate){
Edouard@3019:                 pending_widget_animates.push(this);
Edouard@3019:                 this.pending_animate = true;
Edouard@3019:                 requestHMIAnimation();
Edouard@3019:             }
Edouard@3019: 
Edouard@3019:         }
edouard@3125: 
edouard@3125:         activate_activable(eltsub) {
edouard@3125:             eltsub.inactive.style.display = "none";
edouard@3125:             eltsub.active.style.display = "";
edouard@3125:         }
edouard@3125: 
edouard@3125:         inactivate_activable(eltsub) {
edouard@3125:             eltsub.active.style.display = "none";
edouard@3125:             eltsub.inactive.style.display = "";
edouard@3125:         }
edouard@2949:     }
edouard@2949:     ||
edouard@2949: }
edouard@2949: 
edouard@3128: const "excluded_types", "str:split('Page VarInit VarInitPersistent')";
edouard@3120: 
edouard@3120: // Key to filter unique types
edouard@3120: key "TypesKey", "widget", "@type";
edouard@3120: 
edouard@3007: emit "declarations:hmi-classes" {
edouard@3120:     const "used_widget_types", """$parsed_widgets/widget[
edouard@3120:                                     generate-id() = generate-id(key('TypesKey', @type)) and 
edouard@3120:                                     not(@type = $excluded_types)]""";
edouard@2948:     apply "$used_widget_types", mode="widget_class";
Edouard@2881: }
Edouard@2810: 
edouard@2949: template "widget", mode="widget_class"
edouard@2949: ||
edouard@2949: class «@type»Widget extends Widget{
edouard@2949:     /* empty class, as «@type» widget didn't provide any */
edouard@2949: }
edouard@2949: ||
edouard@2949: 
Edouard@3119: const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id";
Edouard@3121: const "hmi_widgets","$hmi_elements[@id = $included_ids]";
edouard@3162: const "result_widgets","$result_svg_ns//*[@id = $hmi_widgets/@id]";
edouard@2955: 
edouard@3007: emit "declarations:hmi-elements" {
edouard@2941:     | var hmi_widgets = {
Edouard@3121:     apply "$hmi_widgets", mode="hmi_widgets";
edouard@2941:     | }
edouard@2941: }
edouard@2941: 
Edouard@2881: function "defs_by_labels" {
Edouard@2881:     param "labels","''";
Edouard@2881:     param "mandatory","'yes'";
edouard@2920:     param "subelements","/..";
Edouard@2881:     param "hmi_element";
Edouard@2881:     const "widget_type","@type";
Edouard@2881:     foreach "str:split($labels)" {
Edouard@2881:         const "name",".";
edouard@3162:         const "elt","$result_widgets[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]";
Edouard@2881:         choose {
edouard@2920:             when "not($elt/@id)" {
Edouard@2881:                 if "$mandatory='yes'" {
edouard@2920:                     error > «$widget_type» widget must have a «$name» element
Edouard@2834:                 }
Edouard@2881:                 // otherwise produce nothing
Edouard@2797:             }
Edouard@2881:             otherwise {
edouard@2920:                 |     «$name»_elt: id("«$elt/@id»"),
edouard@2920:                 if "$subelements" {
edouard@2920:                 |     «$name»_sub: {
edouard@2920:                     foreach "str:split($subelements)" {
edouard@2920:                         const "subname",".";
edouard@2920:                         const "subelt","$elt/*[@inkscape:label=$subname][1]";
edouard@2920:                         choose {
edouard@2920:                             when "not($subelt/@id)" {
edouard@2920:                                 if "$mandatory='yes'" {
edouard@2920:                                     error > «$widget_type» widget must have a «$name»/«$subname» element
edouard@2920:                                 }
edouard@2920:                 |         /* missing «$name»/«$subname» element */
edouard@2920:                             }
edouard@2920:                             otherwise {
edouard@2920:                 |         "«$subname»": id("«$subelt/@id»")`if "position()!=last()" > ,`
edouard@2920:                             }
edouard@2920:                         }
edouard@2920:                     }
edouard@2920:                 |     },
edouard@2920:                 }
Edouard@2836:             }
Edouard@2808:         }
Edouard@2808:     }
Edouard@2881: }
Edouard@2808: 
edouard@2883: def "func:escape_quotes" {
edouard@2883:     param "txt";
edouard@2883:     // have to use a python string to enter escaped quote
Edouard@3024:     // const "frstln", "string-length($frst)";
edouard@2883:     choose {
Edouard@3024:         when !"contains($txt,'\"')"! {
Edouard@3024:             result !"concat(substring-before($txt,'\"'),'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
edouard@2883:         }
edouard@2883:         otherwise {
edouard@2883:             result "$txt";
edouard@2883:         }
edouard@2883:     }
edouard@2883: }
edouard@2883: