SVGHMI: add support for "enable expressions" with arbitrary variable name assignment.
authorEdouard Tisserant
Tue, 23 Aug 2022 12:19:44 +0200
changeset 3594 30f7eade322f
parent 3593 122b1094b8e6
child 3595 375626e60b63
SVGHMI: add support for "enable expressions" with arbitrary variable name assignment.

HMI tree paths can be prefixed with a variable name "@name=/MY/HMI/VAR"
Widget declarations can end with a "#" followed by a JS expression that refers to name given to variables.
Widget is disabled if expression's result is false.

Commit includes some more-or-less related generated code refactoring, that should simplify extending widget's variables attributes.
svghmi/gen_index_xhtml.ysl2
svghmi/parse_labels.ysl2
svghmi/pythonic.js
svghmi/ui.py
svghmi/widgets_common.ysl2
tests/projects/svghmi_scrollbar/svghmi_0@svghmi/svghmi.svg
--- a/svghmi/gen_index_xhtml.ysl2	Fri Aug 19 10:22:16 2022 +0200
+++ b/svghmi/gen_index_xhtml.ysl2	Tue Aug 23 12:19:44 2022 +0200
@@ -84,6 +84,8 @@
                 // Inline SVG
                 copy "$result_svg";
                 script{
+                    include text pythonic.js
+
                     | \n//\n//\n// Early independent declarations \n//\n//
                     apply "document('')/*/preamble:*";
 
@@ -98,8 +100,6 @@
 
                     include text sprintf.js
 
-                    include text pythonic.js
-
                     include text svghmi.js
 
                     | \n//\n//\n// Declarations from SVG scripts (inkscape document properties) \n//\n//
--- a/svghmi/parse_labels.ysl2	Fri Aug 19 10:22:16 2022 +0200
+++ b/svghmi/parse_labels.ysl2	Tue Aug 23 12:19:44 2022 +0200
@@ -2,7 +2,7 @@
 
 
 //  Parses:
-//  "HMI:WidgetType|freq:param1:param2@path1,path1min,path1max@path2#"
+//  "HMI:WidgetType|freq:param1:param2@a=path1,path1min,path1max@b=path2#a+b>3"
 //
 //  Into:
 //  widget type="WidgetType" id="blah456" {
@@ -13,7 +13,7 @@
 //      path value="path4" index="path4" type="HMI_LOCAL";
 //  }
 //
-const "pathregex",!"'^([^\[,]+)(\[[^\]]+\])?([-.\d,]*)$'"!;
+const "pathregex",!"'^(\w+=)?([^,=]+)([-.\d,]*)$'"!;
 
 const "newline" |
 const "twonewlines", "concat($newline,$newline)";
@@ -66,7 +66,7 @@
         attrib "type" > «$type»
         if "$freq" {
             if "not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))" {
-                error > Widget id:«$id» label:«full_decl» has wrong syntax of frequency forcing «$freq»
+                error > Widget id:«$id» label:«$full_decl» has wrong syntax of frequency forcing «$freq»
             }
             attrib "freq" > «$freq»
         }
@@ -75,39 +75,48 @@
                 attrib "value" > «.»
             }
         }
+        // find "#" + JS expr at the end
         const "tail", "substring-after($declaration,'@')";
         const "taillen","string-length($tail)";
-        const "has_enable", "substring($tail,$taillen,1)='#'";
+        const "has_enable", "contains($tail, '#')";
         const "paths" choose{
             when "$has_enable" {
-               value "substring($tail,1,$taillen - 1)";
+               value "substring-before($tail,'#')";
             }
             otherwise value "$tail";
         }
         if "$has_enable" {
-            attrib "has_enable" > yes
+            const "enable_expr", "substring-after($tail,'#')";
+            attrib "enable_expr" value "$enable_expr";
         }
+
+        // for stricter syntax checking, this should make error
+        // if $paths contains "@@" or ends with "@" (empty paths)
+
         foreach "str:split($paths, '@')" {
             if "string-length(.) > 0" path {
                 // 1 : global match
+                // 2 : assign=
                 // 2 : /path
-                // 3 : [accepts]
-                // 4 : min,max
+                // 3 : min,max
                 const "path_match", "regexp:match(.,$pathregex)";
+                const "pathassign", "substring-before($path_match[2],'=')";
                 const "pathminmax", "str:split($path_match[4],',')";
-                const "path", "$path_match[2]";
-                const "path_accepts", "$path_match[3]";
+                const "path", "$path_match[3]";
                 const "pathminmaxcount", "count($pathminmax)";
-                attrib "value" > «$path»
-                if "string-length($path_accepts)"
-                    attrib "accepts" > «$path_accepts»
+                if "not($path)"
+                    error > Widget id:«$id» label:«$full_decl» has wrong syntax
+
+                attrib "value" value "$path";
+                if "$pathassign"
+                    attrib "assign" value "$pathassign";
                 choose {
                     when "$pathminmaxcount = 2" {
                         attrib "min" > «$pathminmax[1]»
                         attrib "max" > «$pathminmax[2]»
                     }
                     when "$pathminmaxcount = 1 or $pathminmaxcount > 2" {
-                        error > Widget id:«$id» label:«full_decl» has wrong syntax of path section «$pathminmax»
+                        error > Widget id:«$id» label:«$full_decl» has wrong syntax of path section «$pathminmax»
                     }
                 }
                 if "$indexed_hmitree" choose {
@@ -121,7 +130,7 @@
                         const "item", "$indexed_hmitree/*[@hmipath = $path]";
                         const "pathtype", "local-name($item)";
                         if "$pathminmaxcount = 3 and not($pathtype = 'HMI_INT' or $pathtype = 'HMI_REAL')" {
-                            error > Widget id:«$id» label:«full_decl» path section «$pathminmax» use min and max on non mumeric value
+                            error > Widget id:«$id» label:«$full_decl» path section «$pathminmax» use min and max on non mumeric value
                         }
                         if "count($item) = 1" {
                             attrib "index" > «$item/@index»
--- a/svghmi/pythonic.js	Fri Aug 19 10:22:16 2022 +0200
+++ b/svghmi/pythonic.js	Tue Aug 23 12:19:44 2022 +0200
@@ -165,8 +165,11 @@
 }
 
 const _zip = longest => (...iterables) => {
-    if (iterables.length < 2) {
-        throw new TypeError("zip takes 2 iterables at least, "+iterables.length+" given");
+    if (iterables.length == 0) {
+        // works starting with 1 iterable
+        // [a,b,c] -> [[a],[b],[c]]
+        // [a,b,c],[d,e,f] -> [[a,d],[b,e],[c,f]]
+        throw new TypeError("zip takes 1 iterables at least, "+iterables.length+" given");
     }
 
     return new Iterator(function * () {
--- a/svghmi/ui.py	Fri Aug 19 10:22:16 2022 +0200
+++ b/svghmi/ui.py	Tue Aug 23 12:19:44 2022 +0200
@@ -648,14 +648,6 @@
         for path in paths:
             self.AddPathToSignature(path)
 
-        # # TODO DEAD CODE ?
-        # for widget in widgets:
-        #     widget_type = widget.get("type")
-        #     for path in widget.iterchildren("path"):
-        #         path_value = path.get("value")
-        #         path_accepts = map(
-        #             str.strip, path.get("accepts", '')[1:-1].split(','))
-
         self.main_panel.SetupScrolling(scroll_x=False)
 
     def GetWidgetParams(self, _context):
--- a/svghmi/widgets_common.ysl2	Fri Aug 19 10:22:16 2022 +0200
+++ b/svghmi/widgets_common.ysl2	Tue Aug 23 12:19:44 2022 +0200
@@ -65,6 +65,11 @@
     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 {
@@ -84,16 +89,15 @@
                 > «@index»
             }
         }
-        if "position()!=last()" > ,
-    }
-
-    const "minmaxes" foreach "$widget/path" {
-        choose {
-            when "@min and @max"
-                > [«@min»,«@max»]
-            otherwise
-                > undefined
-        }
+        > , {
+        if "@min and @max"{
+                > minmax:[«@min», «@max»]
+                if "@assign"
+                    > ,
+        }
+        if "@assign"
+                > assign:"«@assign»"
+        > }]
         if "position()!=last()" > ,
     }
 
@@ -104,14 +108,34 @@
             > undefined
     }
 
-    const "has_enable" choose {
-        when "$widget/@has_enable = 'yes'"
+    const "enable_expr" choose{
+        when "$widget/@enable_expr"
             > true
         otherwise
             > false
     }
 
-    |   "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$indexes»],[«$minmaxes»],«$has_enable»,{
+    |   "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$variables»],«$enable_expr»,{
+    if "$widget/@enable_expr" {
+
+    |       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.assignments[«position()-1»] = value;
+    |           let «@assign» = this.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()" > ,`
 }
@@ -225,23 +249,22 @@
         unsubscribable = false;
         pending_animate = false;
 
-        constructor(elt_id, freq, args, indexes, minmaxes, has_enable, members){
+        constructor(elt_id, freq, args, variables, enable_expr, members){
             this.element_id = elt_id;
             this.element = id(elt_id);
             this.args = args;
-            this.indexes = indexes;
-            this.indexes_length = indexes.length;
-            this.minmaxes = minmaxes;
-            this.has_enable = has_enable;
+            [this.indexes, this.variables_options] = (variables.length>0) ? zip(...variables) : [[],[]];
+            this.indexes_length = this.indexes.length;
+            this.enable_expr = enable_expr;
             Object.keys(members).forEach(prop => this[prop]=members[prop]);
-            this.lastapply = indexes.map(() => undefined);
-            this.inhibit = indexes.map(() => undefined);
-            this.pending = indexes.map(() => undefined);
+            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 = indexes.map(() => undefined);
-            this.deafen = indexes.map(() => undefined);
-            this.incoming = indexes.map(() => undefined);
+            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;
@@ -277,7 +300,7 @@
                 }
             }
 
-            if(this.has_enable){
+            if(this.enable_expr){
                 this.disabled_elt = null;
                 this.enabled_elts = [];
                 this.enable_state = false;
@@ -363,7 +386,7 @@
         }
 
         clip_min_max(index, new_val) {
-            let minmax = this.minmaxes[index];
+            let minmax = this.variables_options[index].minmax;
             if(minmax !== undefined && typeof new_val == "number") {
                 let [min,max] = minmax;
                 if(new_val < min){
@@ -483,8 +506,7 @@
         _dispatch(value, oldval, varnum) {
             let dispatch = this.dispatch;
             let has_dispatch = dispatch != undefined;
-            let is_enable_var = this.has_enable && (varnum == (this.indexes_length - 1));
-            if(has_dispatch || is_enable_var){
+            if(has_dispatch || this.enable_expr){
                 if(this.deafen[varnum] == undefined){
                     let now = Date.now();
                     let min_interval = 1000/this.frequency;
@@ -496,8 +518,8 @@
                         } catch(err) {
                             console.log(err);
                         }
-                        if(is_enable_var) try {
-                            this.enable(Boolean(value));
+                        if(this.enable_expr) try {
+                            this.compute_enable(value, oldval, varnum);
                         } catch(err) {
                             console.log(err);
                         }
@@ -515,9 +537,9 @@
         }
 
         _animate(){
-            if(this.has_enable)
+            if(this.enable_expr)
                 this.animate_enable();
-            if(this.animate != undefined && (!this.has_enable || this.enable_state))
+            if(this.animate != undefined && (!this.enable_expr || this.enable_state))
                 this.animate();
             this.pending_animate = false;
         }
--- a/tests/projects/svghmi_scrollbar/svghmi_0@svghmi/svghmi.svg	Fri Aug 19 10:22:16 2022 +0200
+++ b/tests/projects/svghmi_scrollbar/svghmi_0@svghmi/svghmi.svg	Tue Aug 23 12:19:44 2022 +0200
@@ -59,9 +59,9 @@
      inkscape:current-layer="hmi0"
      showgrid="false"
      units="px"
-     inkscape:zoom="0.90509668"
-     inkscape:cx="474.80696"
-     inkscape:cy="335.41469"
+     inkscape:zoom="0.64"
+     inkscape:cx="864.62819"
+     inkscape:cy="344.83986"
      inkscape:window-width="1600"
      inkscape:window-height="836"
      inkscape:window-x="0"
@@ -748,10 +748,10 @@
      inkscape:label="HMI:ScrollBar\"
      transform="translate(-202)">
     <desc
-       id="desc150">@.range
-@.position
+       id="desc150">@range=.range
+@pos=.position
 @.size
-#
+#pos&gt;10&amp;&amp;range&gt;50
 
 my tailor is rich</desc>
     <path