# HG changeset patch # User Edouard Tisserant # Date 1661249984 -7200 # Node ID 30f7eade322feadb55675419eea57353a3e0f11d # Parent 122b1094b8e6565bb54ab5d57caf1af82210f627 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. diff -r 122b1094b8e6 -r 30f7eade322f svghmi/gen_index_xhtml.ysl2 --- 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// diff -r 122b1094b8e6 -r 30f7eade322f svghmi/parse_labels.ysl2 --- 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» diff -r 122b1094b8e6 -r 30f7eade322f svghmi/pythonic.js --- 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 * () { diff -r 122b1094b8e6 -r 30f7eade322f svghmi/ui.py --- 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): diff -r 122b1094b8e6 -r 30f7eade322f svghmi/widgets_common.ysl2 --- 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; } diff -r 122b1094b8e6 -r 30f7eade322f tests/projects/svghmi_scrollbar/svghmi_0@svghmi/svghmi.svg --- 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)"> @.range -@.position + id="desc150">@range=.range +@pos=.position @.size -# +#pos>10&&range>50 my tailor is rich