# HG changeset patch # User Edouard Tisserant # Date 1663080795 -7200 # Node ID 51a3d6f39944b8aa54c11664a27d62c441255519 # Parent 02229133df43e858f713f511ae9525adc43d4c14# Parent be87303d5b2d3d413fc071a2f04d1fa68876ba8b Merge changes from default branch diff -r 02229133df43 -r 51a3d6f39944 controls/DebugVariablePanel/DebugVariableItem.py --- a/controls/DebugVariablePanel/DebugVariableItem.py Tue Sep 13 16:51:54 2022 +0200 +++ b/controls/DebugVariablePanel/DebugVariableItem.py Tue Sep 13 16:53:15 2022 +0200 @@ -144,7 +144,7 @@ """ # Return immediately if data empty or none if self.Data is None or self.Data.count == 0: - return [] + return None # Find nearest data outside given range indexes start_idx = (self.GetNearestData(start_tick, -1) @@ -187,6 +187,9 @@ # Get data in given tick range data = self.GetData(start_tick, end_tick) + if data is None: + return None, None, None + # Value range is calculated on whole data if full_range: return data, self.MinValue, self.MaxValue diff -r 02229133df43 -r 51a3d6f39944 exemples/svghmi_jumps/beremiz.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_jumps/beremiz.xml Tue Sep 13 16:53:15 2022 +0200 @@ -0,0 +1,5 @@ + + + + + diff -r 02229133df43 -r 51a3d6f39944 exemples/svghmi_jumps/plc.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_jumps/plc.xml Tue Sep 13 16:53:15 2022 +0200 @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ENTERING_PAGE + + + + + + + BOOL#FALSE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ENTERING_PAGE + + + + + + + LEVEL + + + + + + + + + + + + + CURRENT_PAGE_0 + + + + + + + + + + + ENTERING_PAGE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enable0 + + + + + + + Enable1 + + + + + + + Enable2 + + + + + + + Enable3 + + + + + + + Enable4 + + + + + + + Enable5 + + + + + + + 'VENUS' + + + + + + + 'URANUS' + + + + + + + 'JUPITER' + + + + + + + 'MARS' + + + + + + + 'NEPTUNE' + + + + + + + 'EARTH' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SOME_INPUT + + + + + + + + + + + SOME_OUTPUT + + + + + + + + + + + + + SOME_BOOL + + + + + + + + + + + + + + + + + + diff -r 02229133df43 -r 51a3d6f39944 exemples/svghmi_jumps/svghmi_0@svghmi/baseconfnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_jumps/svghmi_0@svghmi/baseconfnode.xml Tue Sep 13 16:53:15 2022 +0200 @@ -0,0 +1,2 @@ + + diff -r 02229133df43 -r 51a3d6f39944 exemples/svghmi_jumps/svghmi_0@svghmi/confnode.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_jumps/svghmi_0@svghmi/confnode.xml Tue Sep 13 16:53:15 2022 +0200 @@ -0,0 +1,2 @@ + + diff -r 02229133df43 -r 51a3d6f39944 exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg Tue Sep 13 16:53:15 2022 +0200 @@ -0,0 +1,1818 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + HMI:Back + + + Back + + Absolute Page + Home Page + + + + NotificationTest Button + + HMI:PushButton@/ENTERING_PAGE + HMI:Jump:AbsolutePage + + + Normal jump + + HMI:Jump:AbsolutePage:notify=true@notify=/ENTERING_PAGE + + + Jump with notification + + Unconditional Jumps + HMI:Jump:AbsolutePage@enable=/ENABLE_JUMP@level=user_level#enable && level>2 + + + + Jump + + + + Jump + + + Conditional Jumps + Unconditional Jumps > + Conditional Jumps > + + + + + + Home + + + + + Conditional + + + + + Unconditional + + + + + Absolute + + HMI:Jump:Home + HMI:Jump:Conditional + HMI:Jump:Unconditional + HMI:Jump:AbsolutePage + + + + Relative + + HMI:Jump:RelativePage + + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + 0 + User Level + + + declaration of user_level HMI local variable(not a PLC variable) + + + + + ToggleENABLE_JUMP + + 0 + Relative Jumps + + + Jump FB0 + + + Relative Page (%s) + + + + + 1 + + + + 2 + + + + 3 + + + + 4 + + 0 + SOME_INT + + 0 + SOME_BOOL + ? + SOME_STRING + + + + -1 + + + + -.1 + + + + PI + + + + +.1 + + 0 + SOME_REAL + + + +1 + + + + + + >> + + + + + + ? + + + + ? + + + + + + + ? + + + + ? + + + + + + + ? + + + + ? + + + + + << + + + + + + Relative Jumps > + + + Jump FB2 + + + + Jump FB4 + + + + + 0 + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + HMI:Jump:RelativePage@/FB_FOUR + HMI:Jump:RelativePage@/FB_TWO + HMI:Jump:RelativePage@/FB_ZERO + HMI:ForEach:MYNODE@/ + + Notes: - Widget roles are described in objects labels.- Press Ctrl+O to open object properties panel- To see objects in a tree, select Object->Objects in menu- Inkscape's "objects" are SVG elements- Press Ctrl+X to edit SVG elements directly with XML editor + diff -r 02229133df43 -r 51a3d6f39944 svghmi/gen_index_xhtml.ysl2 --- a/svghmi/gen_index_xhtml.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/gen_index_xhtml.ysl2 Tue Sep 13 16:53:15 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 02229133df43 -r 51a3d6f39944 svghmi/parse_labels.ysl2 --- a/svghmi/parse_labels.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/parse_labels.ysl2 Tue Sep 13 16:53:15 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,39 +13,39 @@ // path value="path4" index="path4" type="HMI_LOCAL"; // } // -const "pathregex",!"'^([^\[,]+)(\[[^\]]+\])?([-.\d,]*)$'"!; +const "pathregex",!"'^(\w+=)?([^,=]+)([-.\d,]*)$'"!; const "newline" | const "twonewlines", "concat($newline,$newline)"; template "*", mode="parselabel" { - const "part","@inkscape:label"; + const "label","@inkscape:label"; const "desc", "svg:desc"; // add svg:desc field if continuation "\" marker is found at the end of label - const "len","string-length($part)"; - const "has_continuation", "substring($part,$len,1)='\\'"; - const "label" choose{ + const "len","string-length($label)"; + const "has_continuation", "substring($label,$len,1)='\\'"; + const "full_decl" choose{ when "$has_continuation" { const "_continuation", "substring-before($desc, $twonewlines)"; const "continuation" choose { when "$_continuation" value "$_continuation"; otherwise value "$desc"; } - value "concat(substring($part,1,$len - 1),translate($continuation,$newline,''))"; + value "concat(substring($label,1,$len - 1),translate($continuation,$newline,''))"; } - otherwise value "$part"; + otherwise value "$label"; } const "id","@id"; - const "description", "substring-after($label,'HMI:')"; + const "declaration", "substring-after($full_decl,'HMI:')"; - const "_args", "substring-before($description,'@')"; + const "_args", "substring-before($declaration,'@')"; const "args" choose { when "$_args" value "$_args"; - otherwise value "$description"; + otherwise value "$declaration"; } const "_typefreq", "substring-before($args,':')"; @@ -66,37 +66,59 @@ attrib "type" > «$type» if "$freq" { if "not(regexp:test($freq,'^[0-9]*(\.[0-9]+)?[smh]?'))" { - error > Widget id:«$id» label:«$label» has wrong syntax of frequency forcing «$freq» + error > Widget id:«$id» label:«$full_decl» has wrong syntax of frequency forcing «$freq» } attrib "freq" > «$freq» } + + // find "#" + JS expr at the end + const "tail", "substring-after($declaration,'@')"; + const "taillen","string-length($tail)"; + const "has_enable", "contains($tail, '#')"; + const "paths" choose{ + when "$has_enable" { + value "substring-before($tail,'#')"; + } + otherwise value "$tail"; + } + if "$has_enable" { + const "enable_expr", "substring-after($tail,'#')"; + attrib "enable_expr" value "$enable_expr"; + } + foreach "str:split(substring-after($args, ':'), ':')" { arg { attrib "value" > «.» } } - const "paths", "substring-after($description,'@')"; + + // 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:«$label» 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 { @@ -110,7 +132,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:«$label» 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 02229133df43 -r 51a3d6f39944 svghmi/pythonic.js --- a/svghmi/pythonic.js Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/pythonic.js Tue Sep 13 16:53:15 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 02229133df43 -r 51a3d6f39944 svghmi/svghmi.js --- a/svghmi/svghmi.js Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/svghmi.js Tue Sep 13 16:53:15 2022 +0200 @@ -1,7 +1,5 @@ // svghmi.js -var need_cache_apply = []; - function dispatch_value(index, value) { let widgets = subscribers(index); @@ -86,13 +84,8 @@ } } - while(widget = need_cache_apply.pop()){ - widget.apply_cache(); - } - if(jumps_need_update) update_jumps(); - apply_updates(); pending_widget_animates.forEach(widget => widget._animate()); pending_widget_animates = []; @@ -139,8 +132,9 @@ throw new Error("Unknown index "+index); } }; + + apply_updates(); // register for rendering on next frame, since there are updates - requestHMIAnimation(); } catch(err) { // 1003 is for "Unsupported Data" // ws.close(1003, err.message); @@ -348,14 +342,13 @@ function send_hmi_value(index, value) { if(index > last_remote_index){ - updates.set(index, value); + dispatch_value(index, value); if(persistent_indexes.has(index)){ let varname = persistent_indexes.get(index); document.cookie = varname+"="+value+"; max-age=3153600000"; } - requestHMIAnimation(); return; } diff -r 02229133df43 -r 51a3d6f39944 svghmi/ui.py --- a/svghmi/ui.py Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/ui.py Tue Sep 13 16:53:15 2022 +0200 @@ -662,14 +662,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 02229133df43 -r 51a3d6f39944 svghmi/widget_button.ysl2 --- a/svghmi/widget_button.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/widget_button.ysl2 Tue Sep 13 16:53:15 2022 +0200 @@ -151,7 +151,7 @@ | } } template "show", mode="actions" { - | this.display = "«@eltname»"; + | this.activity_state = «@eltname = 'active'»; | this.request_animate(); } template "hmi-value", mode="actions" { @@ -164,7 +164,6 @@ function "generated_button_class" { param "fsm"; - | display = "inactive"; | state = "init"; | dispatch(value) { @@ -182,14 +181,10 @@ apply "$fsm", mode="actions"; - | animate(){ - | this.set_activation_state(this.display == "active"); - | } - | init() { | this.bound_onmouseup = this.onmouseup.bind(this); | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); - | this.set_activation_state(undefined); + | this.activity_state = undefined; | } } diff -r 02229133df43 -r 51a3d6f39944 svghmi/widget_foreach.ysl2 --- a/svghmi/widget_foreach.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/widget_foreach.ysl2 Tue Sep 13 16:53:15 2022 +0200 @@ -140,7 +140,7 @@ this.unsub_items(); this.sub_items(); update_subscriptions(); - need_cache_apply.push(this); + this.apply_cache(); jumps_need_update = true; requestHMIAnimation(); } diff -r 02229133df43 -r 51a3d6f39944 svghmi/widget_jump.ysl2 --- a/svghmi/widget_jump.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/widget_jump.ysl2 Tue Sep 13 16:53:15 2022 +0200 @@ -3,17 +3,42 @@ widget_desc("Jump") { longdesc || - Jump widget brings focus to a different page. Mandatory single argument + Jump widget brings focus to a different page. Mandatory first argument gives name of the page. - Optional single path is used as new reference when jumping to a relative - page, it must point to a HMI_NODE. + If first path is pointint to HMI_NODE variable is used as new reference + when jumping to a relative page. + + Additional arguments are unordered options: + + - Absolute: force page jump to be not relative even if first path is of type HMI_NODE + + - name=value: Notify PLC about jump by setting variable with path having same name assigned "active"+"inactive" labeled elements can be provided and reflect current page being shown. - "disabled" labeled element, if provided, is shown instead of "active" or - "inactive" widget when pointed HMI_NODE is null. + Exemples: + + Relative jump: + + HMI:Jump:RelativePage@/PUMP9 + HMI:Jump:RelativePage@/PUMP9@role=.userrole#role=="admin" + + Absolute jump: + + HMI:Jump:AbsolutePage + HMI:Jump:AbsolutePage@role=.userrole#role=="admin" + + Forced absolute jump: + + HMI:Jump:AbsolutePage:Absolute@/PUMP9 + HMI:Jump:AbsolutePage:Absolute:notify=1@notify=/PUMP9 + + Jump with feedback + + HMI:Jump:AbsolutePage:notify=1@notify=.did_jump + || shortdesc > Jump to given page @@ -26,110 +51,85 @@ widget_class("Jump") { || activable = false; - active = false; - disabled = false; frequency = 2; - update_activity() { - if(this.active) { - /* show active */ - this.active_elt.style.display = ""; - /* hide inactive */ - this.inactive_elt.style.display = "none"; - } else { - /* show inactive */ - this.inactive_elt.style.display = ""; - /* hide active */ - this.active_elt.style.display = "none"; - } - } - - update_disability() { - if(this.disabled) { - /* show disabled */ - this.disabled_elt.style.display = ""; - /* hide inactive */ - this.inactive_elt.style.display = "none"; - /* hide active */ - this.active_elt.style.display = "none"; - } else { - /* hide disabled */ - this.disabled_elt.style.display = "none"; - this.update_activity(); - } - } - make_on_click() { let that = this; const name = this.args[0]; return function(evt){ - /* TODO: in order to allow jumps to page selected through for exemple a dropdown, - support path pointing to local variable whom value - would be an HMI_TREE index and then jump to a relative page not hard-coded in advance */ - if(!that.disabled) { - const index = that.indexes.length > 0 ? that.indexes[0] + that.offset : undefined; + /* TODO: in order to allow jumps to page selected through + for exemple a dropdown, support path pointing to local + variable whom value would be an HMI_TREE index and then + jump to a relative page not hard-coded in advance + */ + if(that.enable_state) { + const index = + (that.is_relative && that.indexes.length > 0) ? + that.indexes[0] + that.offset : undefined; fading_page_switch(name, index); + that.notify(); } } } notify_page_change(page_name, index) { + // called from animate() if(this.activable) { const ref_index = this.indexes.length > 0 ? this.indexes[0] + this.offset : undefined; const ref_name = this.args[0]; - this.active = ((ref_name == undefined || ref_name == page_name) && index == ref_index); - this.update_state(); + this.activity_state = ((ref_name == undefined || ref_name == page_name) && index == ref_index); + // Since called from animate, update activity directly + if(this.enable_displayed_state && this.has_activity) { + this.animate_activity(); + } } } - - dispatch(value) { - this.disabled = !Number(value); - - // TODO : use RequestAnimate and animate() - - this.update_state(); - } || } +def "func:is_relative_jump" { + param "widget"; + result "$widget/path and $widget/path[1]/@type='HMI_NODE' and not($widget/arg[position()>1 and @value = 'Absolute'])"; +} + widget_defs("Jump") { - // TODO: ensure both active and inactive are provided - const "activity" optional_labels("active inactive"); - const "have_activity","string-length($activity)>0"; - value "$activity"; + optional_activable(); - const "disability" optional_labels("disabled"); - const "have_disability","$have_activity and string-length($disability)>0"; - value "$disability"; + const "jump_disability","$has_activity and $has_disability"; | init: function() { | this.element.onclick = this.make_on_click(); - if "$have_activity" { + if "$has_activity" { | this.activable = true; } - if "not($have_disability)" { - | this.unsubscribable = true; - } - > this.update_state = - choose { - when "$have_disability" { - > this.update_disability - } - when "$have_activity" { - > this.update_activity - } - otherwise > null + + > this.is_relative = + choose{ + when "func:is_relative_jump(.)" > true + otherwise > false } > ;\n | }, + | notify: function() { + const "paths","path"; + foreach "arg[position()>1 and contains(@value,'=')]"{ + const "name","substring-before(@value,'=')"; + const "value","substring-after(@value,'=')"; + const "index" foreach "$paths" if "@assign = $name" value "position()-1"; + | // «@value» + | this.apply_hmi_value(«$index», «$value»); + } + | }, } widget_page("Jump"){ param "page_desc"; - /* check that given path is compatible with page's reference path */ - if "path" { - /* TODO: suport local variable containing an HMI_TREE index to jump to a relative page */ + /* jump is considered relative jump if first path points to HMI_NODE + but a jump can be forced Absolute by adding a "Absolute" argument */ + if "func:is_relative_jump(.)" { + /* if relative check that given path is compatible with page's reference path */ + /* when no page name provided, check for same page */ const "target_page_name" choose { when "arg" value "arg[1]/@value"; @@ -142,9 +142,12 @@ if "not(func:same_class_paths($target_page_path, path[1]/@value))" error > Jump id="«@id»" to page "«$target_page_name»" with incompatible path "«path[1]/@value» (must be same class as "«$target_page_path»") + } } + + /* TODO: move to detachable pages ysl2 */ emit "cssdefs:jump" || @@ -165,6 +168,7 @@ var jump_history = [[default_page, undefined]]; function update_jumps() { + // called from animate() page_desc[current_visible_page].jumps.map(w=>w.notify_page_change(current_visible_page,current_page_index)); jumps_need_update = false; }; diff -r 02229133df43 -r 51a3d6f39944 svghmi/widget_keypad.ysl2 --- a/svghmi/widget_keypad.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/widget_keypad.ysl2 Tue Sep 13 16:53:15 2022 +0200 @@ -118,11 +118,11 @@ } if(this.Shift_sub && this.shift != this._shift){ this._shift = this.shift; - set_activation_state(this.Shift_sub, this.shift); + set_activity_state(this.Shift_sub, this.shift); } if(this.CapsLock_sub && this.caps != this._caps){ this._caps = this.caps; - set_activation_state(this.CapsLock_sub, this.caps); + set_activity_state(this.CapsLock_sub, this.caps); } } || diff -r 02229133df43 -r 51a3d6f39944 svghmi/widget_tooglebutton.ysl2 --- a/svghmi/widget_tooglebutton.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/widget_tooglebutton.ysl2 Tue Sep 13 16:53:15 2022 +0200 @@ -18,33 +18,27 @@ widget_class("ToggleButton") { || frequency = 5; - state = 0; active_style = undefined; inactive_style = undefined; dispatch(value) { - this.state = value; + this.activity_state = Boolean(value); //redraw toggle button this.request_animate(); } on_click(evt) { //toggle state and apply - this.state = this.state ? false : true; - this.apply_hmi_value(0, this.state); + this.activity_state = this.activity_state ? false : true; + this.apply_hmi_value(0, this.activity_state); //redraw toggle button this.request_animate(); } - animate(){ - // redraw toggle button on screen refresh - this.set_activation_state(this.state); - } - init() { this.element.onclick = (evt) => this.on_click(evt); - this.set_activation_state(undefined); + this.activity_state = undefined; } || } diff -r 02229133df43 -r 51a3d6f39944 svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Tue Sep 13 16:51:54 2022 +0200 +++ b/svghmi/widgets_common.ysl2 Tue Sep 13 16:53:15 2022 +0200 @@ -21,13 +21,25 @@ } }; +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_sub:{ - warning_labels("/active /inactive") { - content; - } - | } -}; + _activable("warn") +}; +decl optional_activable() alias - { + _activable("no") +}; + decl activable_labels(*ptr) alias - { optional_labels(*ptr) { with "subelements","'active inactive'"; @@ -48,6 +60,10 @@ 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; }; @@ -65,6 +81,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 +105,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,7 +124,34 @@ > undefined } - | "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$indexes»],[«$minmaxes»],{ + 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" { + + | 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()" > ,` } @@ -187,7 +234,7 @@ placeholder.parentNode.insertBefore(elt, placeholder); } - function set_activation_state(eltsub, state){ + function set_activity_state(eltsub, state){ if(eltsub.active_elt != undefined){ if(eltsub.active_elt_placeholder == undefined){ eltsub.active_elt_placeholder = document.createComment(""); @@ -204,35 +251,34 @@ } } - function activate_activable(eltsub) { - set_activation_state(eltsub, true); - } - - function inactivate_activable(eltsub) { - set_activation_state(eltsub, false); - } - class Widget { offset = 0; frequency = 10; /* FIXME arbitrary default max freq. Obtain from config ? */ unsubscribable = false; pending_animate = false; - constructor(elt_id, freq, args, indexes, minmaxes, 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.minmaxes = minmaxes; + + [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 = 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; @@ -267,30 +313,39 @@ 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 */ - if(!this.unsubscribable) - 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.indexes[i]; - if(this.relativeness[i]) - index += this.offset; - subscribers(index).delete(this); - } + 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; } @@ -300,23 +355,22 @@ this.relativeness = relativeness; this.container_id = container_id ; /* add this's subsribers */ - if(!this.unsubscribable) - for(let i = 0; i < this.indexes.length; i++) { - let index = this.get_variable_index(i); - if(index == undefined) continue; - subscribers(index).add(this); - } - need_cache_apply.push(this); + 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() { - if(!this.unsubscribable) for(let index in this.indexes){ + for(let i = 0; i < this.indexes_length; i++) { /* dispatch current cache in newly opened page widgets */ - let realindex = this.get_variable_index(index); + let realindex = this.get_variable_index(i); if(realindex == undefined) continue; let cached_val = cache[realindex]; if(cached_val != undefined) - this._dispatch(cached_val, cached_val, index); + this.feed_data_for_dispatch(cached_val, cached_val, i); } } @@ -339,7 +393,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){ @@ -402,12 +456,12 @@ new_hmi_value(index, value, oldval) { // TODO avoid searching, store index at sub() - for(let i = 0; i < this.indexes.length; i++) { + for(let i = 0; i < this.indexes_length; i++) { let refindex = this.get_variable_index(i); if(refindex == undefined) continue; if(index == refindex) { - this._dispatch(value, oldval, i); + this.feed_data_for_dispatch(value, oldval, i); break; } } @@ -417,23 +471,57 @@ this.deafen[index] = undefined; let [new_val, old_val] = this.incoming[index]; this.incoming[index] = undefined; - this.dispatch(new_val, old_val, index); - } - - _dispatch(value, oldval, varnum) { - let dispatch = this.dispatch; - if(dispatch != undefined){ + 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; - try { - dispatch.call(this, value, oldval, varnum); - } catch(err) { - console.log(err); - } + this.do_dispatch(value, oldval, varnum) } else { let elapsed = now - lastdispatch; @@ -447,8 +535,29 @@ } } + 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(){ - this.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; } @@ -460,8 +569,11 @@ } } - set_activation_state(state){ - set_activation_state(this.activable_sub, state); + 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; + } } } || diff -r 02229133df43 -r 51a3d6f39944 tests/projects/svghmi/plc.xml --- a/tests/projects/svghmi/plc.xml Tue Sep 13 16:51:54 2022 +0200 +++ b/tests/projects/svghmi/plc.xml Tue Sep 13 16:53:15 2022 +0200 @@ -1,7 +1,7 @@ - + @@ -575,64 +575,6 @@ - - - - - - Sloth - - - - - - - - - - - Pressure - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 100 - @@ -688,7 +630,7 @@ - TargetPressure + Pressure diff -r 02229133df43 -r 51a3d6f39944 tests/projects/svghmi/svghmi_0@svghmi/confnode.xml --- a/tests/projects/svghmi/svghmi_0@svghmi/confnode.xml Tue Sep 13 16:51:54 2022 +0200 +++ b/tests/projects/svghmi/svghmi_0@svghmi/confnode.xml Tue Sep 13 16:53:15 2022 +0200 @@ -1,2 +1,2 @@ - + diff -r 02229133df43 -r 51a3d6f39944 tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Tue Sep 13 16:51:54 2022 +0200 +++ b/tests/projects/svghmi/svghmi_0@svghmi/svghmi.svg Tue Sep 13 16:53:15 2022 +0200 @@ -136,9 +136,9 @@ inkscape:current-layer="hmi0" showgrid="false" units="px" - inkscape:zoom="0.14174805" - inkscape:cx="-1530.0784" - inkscape:cy="-1404.9832" + inkscape:zoom="0.40092403" + inkscape:cx="323.58553" + inkscape:cy="-56.756946" inkscape:window-width="1600" inkscape:window-height="836" inkscape:window-x="0" @@ -3240,7 +3240,7 @@ inkscape:label="PUMP:1"> + Disabled + Inactive - - Pump + id="tspan1460-3-6">Active + + + 8888 + + + 8888 + + + 8888 + x="1093.7899" + y="657.59668" + style="text-align:center;text-anchor:middle;fill:#434343;fill-opacity:1;stroke-width:0.99999994px" + id="tspan1460-3-5-9">Disabled - - - - + id="g1067-1" + inkscape:label="inactive" + style="display:inline"> + Inactive + id="rect1069-6" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#e6e6e6;fill-opacity:1;fill-rule:nonzero;stroke:#ff6600;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + Active + + + + + 8888 + + + + Disabled + Inactive - Pump - 8888 - - - - - - + inkscape:label="active" + id="g1071-7" + style="display:inline"> + Active + + + + + 8888 + + + + Disabled + id="g1067-12" + inkscape:label="inactive" + style="display:inline"> + Inactive + id="g1071-78" + style="display:inline"> - - Pump - 8888 - - - - - - - - - - - - - - - - Pump - 8888 + id="tspan1460-3-6-0">Active @@ -8450,4 +8526,35 @@ style="text-align:center;text-anchor:middle;fill:#ff6600;stroke-width:0.99999994px">up + + + + + + Home + + diff -r 02229133df43 -r 51a3d6f39944 tests/projects/svghmi_scrollbar/plc.xml --- a/tests/projects/svghmi_scrollbar/plc.xml Tue Sep 13 16:51:54 2022 +0200 +++ b/tests/projects/svghmi_scrollbar/plc.xml Tue Sep 13 16:53:15 2022 +0200 @@ -1,7 +1,7 @@ - + diff -r 02229133df43 -r 51a3d6f39944 tests/projects/svghmi_scrollbar/svghmi_0@svghmi/svghmi.svg --- a/tests/projects/svghmi_scrollbar/svghmi_0@svghmi/svghmi.svg Tue Sep 13 16:51:54 2022 +0200 +++ b/tests/projects/svghmi_scrollbar/svghmi_0@svghmi/svghmi.svg Tue Sep 13 16:53:15 2022 +0200 @@ -60,8 +60,8 @@ showgrid="false" units="px" inkscape:zoom="0.64" - inkscape:cx="476.03774" - inkscape:cy="444.53549" + inkscape:cx="864.62819" + inkscape:cy="344.83986" inkscape:window-width="1600" inkscape:window-height="836" inkscape:window-x="0" @@ -748,11 +748,18 @@ inkscape:label="HMI:ScrollBar\" transform="translate(-202)"> @.range -@.position + id="desc150">@range=.range +@pos=.position @.size +#pos>10&&range>50 my tailor is rich +