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.
--- 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>10&&range>50
my tailor is rich</desc>
<path