# HG changeset patch # User Edouard Tisserant # Date 1596646169 -7200 # Node ID ff9ae4f4e3be517bd997ca09a356af977517b562 # Parent 705e34c6fe93c813fa027a9261e8e4b61cbd5cb7 SVGHMI: widgets are not anymore binary relative or absolute, but have a "relativeness". Because of allowing multiple variables per widget, we must distinguish if individual variables is relative to page, not the whole widget. diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/detachable_pages.ysl2 --- a/svghmi/detachable_pages.ysl2 Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/detachable_pages.ysl2 Wed Aug 05 18:49:29 2020 +0200 @@ -138,14 +138,14 @@ warning > Page id="«$page/@id»" : No match for path "«$desc/path/@value»" in HMI tree | page_index: «$desc/path/@index», } - | relative_widgets: [ - foreach "$page_relative_widgets" { - | hmi_widgets["«@id»"]`if "position()!=last()" > ,` - } - | ], - | absolute_widgets: [ - foreach "$page_managed_widgets[not(@id = $page_relative_widgets/@id)]" { - | hmi_widgets["«@id»"]`if "position()!=last()" > ,` + | widgets: [ + foreach "$page_managed_widgets" { + const "widget_paths_relativeness" + foreach "func:widget(@id)/path" { + value "func:is_descendant_path(@value, $desc/path/@value)"; + if "position()!=last()" > , + } + | [hmi_widgets["«@id»"], [«$widget_paths_relativeness»]]`if "position()!=last()" > ,` } | ], | jumps: [ diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/gen_index_xhtml.xslt Wed Aug 05 18:49:29 2020 +0200 @@ -497,26 +497,22 @@ , - relative_widgets: [ - - - hmi_widgets[" + widgets: [ + + + + + + + , + + + + [hmi_widgets[" - "] - - , - - - - - ], - - absolute_widgets: [ - - - hmi_widgets[" - - "] + "], [ + + ]] , @@ -947,80 +943,152 @@ /* remove subsribers */ + if(!this.unsubscribable) + + for(let i = 0; i < this.indexes.length; i++) { + + let index = this.indexes[i]; + + if(this.relativeness[i]) + + index += this.offset; + + subscribers[index].delete(this); + + } + + this.offset = 0; + + this.relativeness = undefined; + + } + + + + sub(new_offset=0, relativeness){ + + this.offset = new_offset; + + this.relativeness = relativeness; + + /* add this's subsribers */ + + if(!this.unsubscribable) + + for(let i = 0; i < this.indexes.length; i++) { + + let index = this.indexes[i]; + + if(relativeness[i]) + + index += new_offset; + + subscribers[index].add(this); + + } + + need_cache_apply.push(this); + + } + + + + apply_cache() { + if(!this.unsubscribable) for(let index of this.indexes){ - let idx = index + this.offset; - - subscribers[idx].delete(this); + /* dispatch current cache in newly opened page widgets */ + + let realindex = index+this.offset; + + let cached_val = cache[realindex]; + + if(cached_val != undefined) + + this.new_hmi_value(realindex, cached_val, cached_val); } - this.offset = 0; - } - sub(new_offset=0){ - - /* set the offset because relative */ - - this.offset = new_offset; - - /* add this's subsribers */ - - if(!this.unsubscribable) for(let index of this.indexes){ - - subscribers[index + new_offset].add(this); + get_idx(index) { + + let orig = this.indexes[index]; + + return this.relativeness[index] ? orig + this.offset : orig; + + } + + change_hmi_value(index,opstr) { + + return change_hmi_value(this.get_idx(index), opstr); + + } + + + + apply_hmi_value(index, new_val) { + + return apply_hmi_value(this.get_idx(0), new_val); + + } + + + + new_hmi_value(index, value, oldval) { + + try { + + // TODO avoid searching, store index at sub() + + for(let i = 0; i < this.indexes.length; i++) { + + let refindex = this.indexes[i]; + + if(this.relativeness[i]) + + refindex += this.offset; + + + + if(index == refindex) { + + let d = this.dispatch; + + if(typeof(d) == "function"){ + + d.call(this, value, oldval, i); + + } + + else if(typeof(d) == "object"){ + + d[i].call(this, value, oldval); + + } + + /* else dispatch_0, ..., dispatch_n ? */ + + /*else { + + throw new Error("Dunno how to dispatch to widget at index = " + index); + + }*/ + + break; + + } + + } + + } catch(err) { + + console.log(err); } - need_cache_apply.push(this); - - } - - - - apply_cache() { - - if(!this.unsubscribable) for(let index of this.indexes){ - - /* dispatch current cache in newly opened page widgets */ - - let realindex = index+this.offset; - - let cached_val = cache[realindex]; - - if(cached_val != undefined) - - dispatch_value_to_widget(this, realindex, cached_val, cached_val); - - } - - } - - - - get_idx(index) { - - let orig = this.indexes[index]; - - return this.offset ? orig + this.offset : orig; - - } - - change_hmi_value(index,opstr) { - - return change_hmi_value(this.get_idx(index), opstr); - - } - - - - apply_hmi_value(index, new_val) { - - return apply_hmi_value(this.get_idx(0), new_val); - } } @@ -1892,6 +1960,20 @@ + + + ForEach widget + + must have one HMI path given. + + + + + ForEach widget + + must have one argument given : a class name. + + @@ -1993,97 +2075,125 @@ class ForEachWidget extends Widget{ + + + unsub_items(){ + + for(let item of this.items){ + + for(let widget of item) { + + widget.unsub(); + + } + + } + + } + + + unsub(){ - for(let item of this.items){ + this.unsub_items(); + + this.offset = 0; + + this.relativeness = undefined; + + } + + + + sub_items(){ + + for(let i = 0; i < this.items.length; i++) { + + let item = this.items[i]; + + let orig_item_index = this.index_pool[i]; + + let item_index = this.index_pool[i+this.item_offset]; + + let item_index_offset = item_index - orig_item_index; + + if(this.relativeness[0]) + + item_index_offset += this.offset; for(let widget of item) { - widget.unsub(); + /* all variables of all widgets in a ForEach are all relative. + + Really. + + + + TODO: allow absolute variables in ForEach widgets + + */ + + widget.sub(item_index_offset, widget.indexes.map(_=>true)); } } - this.offset = 0; - } - foreach_widgets_do(todo){ - - for(let i = 0; i < this.items.length; i++) { - - let item = this.items[i]; - - let orig_item_index = this.index_pool[i]; - - let item_index = this.index_pool[i+this.item_offset]; - - let item_index_offset = item_index - orig_item_index; - - for(let widget of item) { - - todo(widget).call(widget, this.offset + item_index_offset); - - } + sub(new_offset=0, relativeness=[]){ + + this.offset = new_offset; + + this.relativeness = relativeness; + + this.sub_items(); + + } + + + + apply_cache() { + + this.items.forEach(item=>item.forEach(widget=>widget.apply_cache())); + + } + + + + on_click(opstr, evt) { + + let new_item_offset = eval(String(this.item_offset)+opstr); + + if(new_item_offset + this.items.length > this.index_pool.length) { + + if(this.item_offset + this.items.length == this.index_pool.length) + + new_item_offset = 0; + + else + + new_item_offset = this.index_pool.length - this.items.length; + + } else if(new_item_offset < 0) { + + if(this.item_offset == 0) + + new_item_offset = this.index_pool.length - this.items.length; + + else + + new_item_offset = 0; } - } - - - - sub(new_offset=0){ - - this.offset = new_offset; - - this.foreach_widgets_do(w=>w.sub); - - } - - - - apply_cache() { - - this.foreach_widgets_do(w=>w.apply_cache); - - } - - - - on_click(opstr, evt) { - - let new_item_offset = eval(String(this.item_offset)+opstr); - - if(new_item_offset + this.items.length > this.index_pool.length) { - - if(this.item_offset + this.items.length == this.index_pool.length) - - new_item_offset = 0; - - else - - new_item_offset = this.index_pool.length - this.items.length; - - } else if(new_item_offset < 0) { - - if(this.item_offset == 0) - - new_item_offset = this.index_pool.length - this.items.length; - - else - - new_item_offset = 0; - - } - this.item_offset = new_item_offset; - this.unsub(); - - this.sub(this.offset); + this.unsub_items(); + + this.sub_items(); update_subscriptions(); @@ -3049,655 +3159,613 @@ - function dispatch_value_to_widget(widget, index, value, oldval) { + + + function dispatch_value(index, value) { + + let widgets = subscribers[index]; + + + + let oldval = cache[index]; + + cache[index] = value; + + + + if(widgets.size > 0) { + + for(let widget of widgets){ + + widget.new_hmi_value(index, value, oldval); + + } + + } + + }; + + + + function init_widgets() { + + Object.keys(hmi_widgets).forEach(function(id) { + + let widget = hmi_widgets[id]; + + let init = widget.init; + + if(typeof(init) == "function"){ + + try { + + init.call(widget); + + } catch(err) { + + console.log(err); + + } + + } + + }); + + }; + + + + // Open WebSocket to relative "/ws" address + + var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws')); + + ws.binaryType = 'arraybuffer'; + + + + const dvgetters = { + + INT: (dv,offset) => [dv.getInt16(offset, true), 2], + + BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], + + NODE: (dv,offset) => [dv.getInt8(offset, true), 1], + + STRING: (dv, offset) => { + + size = dv.getInt8(offset); + + return [ + + String.fromCharCode.apply(null, new Uint8Array( + + dv.buffer, /* original buffer */ + + offset + 1, /* string starts after size*/ + + size /* size of string */ + + )), size + 1]; /* total increment */ + + } + + }; + + + + // Apply updates recieved through ws.onmessage to subscribed widgets + + function apply_updates() { + + for(let index in updates){ + + // serving as a key, index becomes a string + + // -> pass Number(index) instead + + dispatch_value(Number(index), updates[index]); + + delete updates[index]; + + } + + } + + + + // Called on requestAnimationFrame, modifies DOM + + var requestAnimationFrameID = null; + + function animate() { + + // Do the page swith if any one pending + + if(current_subscribed_page != current_visible_page){ + + switch_visible_page(current_subscribed_page); + + } + + + + while(widget = need_cache_apply.pop()){ + + widget.apply_cache(); + + } + + + + if(jumps_need_update) update_jumps(); + + + + apply_updates(); + + requestAnimationFrameID = null; + + } + + + + function requestHMIAnimation() { + + if(requestAnimationFrameID == null){ + + requestAnimationFrameID = window.requestAnimationFrame(animate); + + } + + } + + + + // Message reception handler + + // Hash is verified and HMI values updates resulting from binary parsing + + // are stored until browser can compute next frame, DOM is left untouched + + ws.onmessage = function (evt) { + + + + let data = evt.data; + + let dv = new DataView(data); + + let i = 0; try { - let idx = widget.offset ? index - widget.offset : index; - - let idxidx = widget.indexes.indexOf(idx); - - let d = widget.dispatch; - - if(typeof(d) == "function" && idxidx == 0){ - - d.call(widget, value, oldval); + for(let hash_int of hmi_hash) { + + if(hash_int != dv.getUint8(i)){ + + throw new Error("Hash doesn't match"); + + }; + + i++; + + }; + + + + while(i < data.byteLength){ + + let index = dv.getUint32(i, true); + + i += 4; + + let iectype = hmitree_types[index]; + + if(iectype != undefined){ + + let dvgetter = dvgetters[iectype]; + + let [value, bytesize] = dvgetter(dv,i); + + updates[index] = value; + + i += bytesize; + + } else { + + throw new Error("Unknown index "+index); + + } + + }; + + // register for rendering on next frame, since there are updates + + requestHMIAnimation(); + + } catch(err) { + + // 1003 is for "Unsupported Data" + + // ws.close(1003, err.message); + + + + // TODO : remove debug alert ? + + alert("Error : "+err.message+"\nHMI will be reloaded."); + + + + // force reload ignoring cache + + location.reload(true); + + } + + }; + + + + + + function send_blob(data) { + + if(data.length > 0) { + + ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data))); + + }; + + }; + + + + const typedarray_types = { + + INT: (number) => new Int16Array([number]), + + BOOL: (truth) => new Int16Array([truth]), + + NODE: (truth) => new Int16Array([truth]), + + STRING: (str) => { + + // beremiz default string max size is 128 + + str = str.slice(0,128); + + binary = new Uint8Array(str.length + 1); + + binary[0] = str.length; + + for(var i = 0; i < str.length; i++){ + + binary[i+1] = str.charCodeAt(i); } - else if(typeof(d) == "object" && d.length >= idxidx){ - - d[idxidx].call(widget, value, oldval); + return binary; + + } + + /* TODO */ + + }; + + + + function send_reset() { + + send_blob(new Uint8Array([1])); /* reset = 1 */ + + }; + + + + // subscription state, as it should be in hmi server + + // hmitree indexed array of integers + + var subscriptions = hmitree_types.map(_ignored => 0); + + + + // subscription state as needed by widget now + + // hmitree indexed array of Sets of widgets objects + + var subscribers = hmitree_types.map(_ignored => new Set()); + + + + // artificially subscribe the watchdog widget to "/heartbeat" hmi variable + + // Since dispatch directly calls change_hmi_value, + + // PLC will periodically send variable at given frequency + + subscribers[heartbeat_index].add({ + + /* type: "Watchdog", */ + + frequency: 1, + + indexes: [heartbeat_index], + + new_hmi_value: function(index, value, oldval) { + + apply_hmi_value(heartbeat_index, value+1); + + } + + }); + + + + function update_subscriptions() { + + let delta = []; + + for(let index = 0; index < subscribers.length; index++){ + + let widgets = subscribers[index]; + + + + // periods are in ms + + let previous_period = subscriptions[index]; + + + + // subscribing with a zero period is unsubscribing + + let new_period = 0; + + if(widgets.size > 0) { + + let maxfreq = 0; + + for(let widget of widgets){ + + let wf = widget.frequency; + + if(wf != undefined && maxfreq < wf) + + maxfreq = wf; + + } + + + + if(maxfreq != 0) + + new_period = 1000/maxfreq; } - /* else dispatch_0, ..., dispatch_n ? */ - - /*else { - - throw new Error("Dunno how to dispatch to widget at index = " + index); - - }*/ - - } catch(err) { - - console.log(err); + + + if(previous_period != new_period) { + + subscriptions[index] = new_period; + + delta.push( + + new Uint8Array([2]), /* subscribe = 2 */ + + new Uint32Array([index]), + + new Uint16Array([new_period])); + + } } + send_blob(delta); + + }; + + + + function send_hmi_value(index, value) { + + let iectype = hmitree_types[index]; + + let tobinary = typedarray_types[iectype]; + + send_blob([ + + new Uint8Array([0]), /* setval = 0 */ + + new Uint32Array([index]), + + tobinary(value)]); + + + + // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf + + // cache[index] = value; + + }; + + + + function apply_hmi_value(index, new_val) { + + let old_val = cache[index] + + if(new_val != undefined && old_val != new_val) + + send_hmi_value(index, new_val); + + return new_val; + } - function dispatch_value(index, value) { - - let widgets = subscribers[index]; - - - - let oldval = cache[index]; - - cache[index] = value; - - - - if(widgets.size > 0) { - - for(let widget of widgets){ - - dispatch_value_to_widget(widget, index, value, oldval); + quotes = {"'":null, '"':null}; + + + + function change_hmi_value(index, opstr) { + + let op = opstr[0]; + + let given_val; + + if(opstr.length < 2) + + return undefined; // TODO raise + + if(opstr[1] in quotes){ + + if(opstr.length < 3) + + return undefined; // TODO raise + + if(opstr[opstr.length-1] == opstr[1]){ + + given_val = opstr.slice(2,opstr.length-1); } + } else { + + given_val = Number(opstr.slice(1)); + } + let old_val = cache[index]; + + let new_val; + + switch(op){ + + case "=": + + new_val = given_val; + + break; + + case "+": + + new_val = old_val + given_val; + + break; + + case "-": + + new_val = old_val - given_val; + + break; + + case "*": + + new_val = old_val * given_val; + + break; + + case "/": + + new_val = old_val / given_val; + + break; + + } + + if(new_val != undefined && old_val != new_val) + + send_hmi_value(index, new_val); + + // TODO else raise + + return new_val; + + } + + + + var current_visible_page; + + var current_subscribed_page; + + var current_page_index; + + + + function prepare_svg() { + + for(let eltid in detachable_elements){ + + let [element,parent] = detachable_elements[eltid]; + + parent.removeChild(element); + + } + }; - function init_widgets() { - - Object.keys(hmi_widgets).forEach(function(id) { - - let widget = hmi_widgets[id]; - - let init = widget.init; - - if(typeof(init) == "function"){ - - try { - - init.call(widget); - - } catch(err) { - - console.log(err); - - } - - } - - }); - - }; - - - - // Open WebSocket to relative "/ws" address - - var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws')); - - ws.binaryType = 'arraybuffer'; - - - - const dvgetters = { - - INT: (dv,offset) => [dv.getInt16(offset, true), 2], - - BOOL: (dv,offset) => [dv.getInt8(offset, true), 1], - - NODE: (dv,offset) => [dv.getInt8(offset, true), 1], - - STRING: (dv, offset) => { - - size = dv.getInt8(offset); - - return [ - - String.fromCharCode.apply(null, new Uint8Array( - - dv.buffer, /* original buffer */ - - offset + 1, /* string starts after size*/ - - size /* size of string */ - - )), size + 1]; /* total increment */ + function switch_page(page_name, page_index) { + + if(current_subscribed_page != current_visible_page){ + + /* page switch already going */ + + /* TODO LOG ERROR */ + + return false; } - }; - - - - // Apply updates recieved through ws.onmessage to subscribed widgets - - function apply_updates() { - - for(let index in updates){ - - // serving as a key, index becomes a string - - // -> pass Number(index) instead - - dispatch_value(Number(index), updates[index]); - - delete updates[index]; + + + if(page_name == undefined) + + page_name = current_subscribed_page; + + + + + + let old_desc = page_desc[current_subscribed_page]; + + let new_desc = page_desc[page_name]; + + + + if(new_desc == undefined){ + + /* TODO LOG ERROR */ + + return false; } - } - - - - // Called on requestAnimationFrame, modifies DOM - - var requestAnimationFrameID = null; - - function animate() { - - // Do the page swith if any one pending - - if(current_subscribed_page != current_visible_page){ - - switch_visible_page(current_subscribed_page); + + + if(page_index == undefined){ + + page_index = new_desc.page_index; } - while(widget = need_cache_apply.pop()){ - - widget.apply_cache(); + if(old_desc){ + + old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); } - - - if(jumps_need_update) update_jumps(); - - - - apply_updates(); - - requestAnimationFrameID = null; - - } - - - - function requestHMIAnimation() { - - if(requestAnimationFrameID == null){ - - requestAnimationFrameID = window.requestAnimationFrame(animate); - - } - - } - - - - // Message reception handler - - // Hash is verified and HMI values updates resulting from binary parsing - - // are stored until browser can compute next frame, DOM is left untouched - - ws.onmessage = function (evt) { - - - - let data = evt.data; - - let dv = new DataView(data); - - let i = 0; - - try { - - for(let hash_int of hmi_hash) { - - if(hash_int != dv.getUint8(i)){ - - throw new Error("Hash doesn't match"); - - }; - - i++; - - }; - - - - while(i < data.byteLength){ - - let index = dv.getUint32(i, true); - - i += 4; - - let iectype = hmitree_types[index]; - - if(iectype != undefined){ - - let dvgetter = dvgetters[iectype]; - - let [value, bytesize] = dvgetter(dv,i); - - updates[index] = value; - - i += bytesize; - - } else { - - throw new Error("Unknown index "+index); - - } - - }; - - // register for rendering on next frame, since there are updates - - requestHMIAnimation(); - - } catch(err) { - - // 1003 is for "Unsupported Data" - - // ws.close(1003, err.message); - - - - // TODO : remove debug alert ? - - alert("Error : "+err.message+"\nHMI will be reloaded."); - - - - // force reload ignoring cache - - location.reload(true); - - } - - }; - - - - - - function send_blob(data) { - - if(data.length > 0) { - - ws.send(new Blob([new Uint8Array(hmi_hash)].concat(data))); - - }; - - }; - - - - const typedarray_types = { - - INT: (number) => new Int16Array([number]), - - BOOL: (truth) => new Int16Array([truth]), - - NODE: (truth) => new Int16Array([truth]), - - STRING: (str) => { - - // beremiz default string max size is 128 - - str = str.slice(0,128); - - binary = new Uint8Array(str.length + 1); - - binary[0] = str.length; - - for(var i = 0; i < str.length; i++){ - - binary[i+1] = str.charCodeAt(i); - - } - - return binary; - - } - - /* TODO */ - - }; - - - - function send_reset() { - - send_blob(new Uint8Array([1])); /* reset = 1 */ - - }; - - - - // subscription state, as it should be in hmi server - - // hmitree indexed array of integers - - var subscriptions = hmitree_types.map(_ignored => 0); - - - - // subscription state as needed by widget now - - // hmitree indexed array of Sets of widgets objects - - var subscribers = hmitree_types.map(_ignored => new Set()); - - - - // artificially subscribe the watchdog widget to "/heartbeat" hmi variable - - // Since dispatch directly calls change_hmi_value, - - // PLC will periodically send variable at given frequency - - subscribers[heartbeat_index].add({ - - /* type: "Watchdog", */ - - frequency: 1, - - indexes: [heartbeat_index], - - dispatch: function(value) { - - apply_hmi_value(heartbeat_index, value+1); - - } - - }); - - - - function update_subscriptions() { - - let delta = []; - - for(let index = 0; index < subscribers.length; index++){ - - let widgets = subscribers[index]; - - - - // periods are in ms - - let previous_period = subscriptions[index]; - - - - // subscribing with a zero period is unsubscribing - - let new_period = 0; - - if(widgets.size > 0) { - - let maxfreq = 0; - - for(let widget of widgets){ - - let wf = widget.frequency; - - if(wf != undefined && maxfreq < wf) - - maxfreq = wf; - - } - - - - if(maxfreq != 0) - - new_period = 1000/maxfreq; - - } - - - - if(previous_period != new_period) { - - subscriptions[index] = new_period; - - delta.push( - - new Uint8Array([2]), /* subscribe = 2 */ - - new Uint32Array([index]), - - new Uint16Array([new_period])); - - } - - } - - send_blob(delta); - - }; - - - - function send_hmi_value(index, value) { - - let iectype = hmitree_types[index]; - - let tobinary = typedarray_types[iectype]; - - send_blob([ - - new Uint8Array([0]), /* setval = 0 */ - - new Uint32Array([index]), - - tobinary(value)]); - - - - // DON'T DO THAT unless read_iterator in svghmi.c modifies wbuf as well, not only rbuf - - // cache[index] = value; - - }; - - - - function apply_hmi_value(index, new_val) { - - let old_val = cache[index] - - if(new_val != undefined && old_val != new_val) - - send_hmi_value(index, new_val); - - return new_val; - - } - - - - quotes = {"'":null, '"':null}; - - - - function change_hmi_value(index, opstr) { - - let op = opstr[0]; - - let given_val; - - if(opstr.length < 2) - - return undefined; // TODO raise - - if(opstr[1] in quotes){ - - if(opstr.length < 3) - - return undefined; // TODO raise - - if(opstr[opstr.length-1] == opstr[1]){ - - given_val = opstr.slice(2,opstr.length-1); - - } - - } else { - - given_val = Number(opstr.slice(1)); - - } - - let old_val = cache[index]; - - let new_val; - - switch(op){ - - case "=": - - new_val = given_val; - - break; - - case "+": - - new_val = old_val + given_val; - - break; - - case "-": - - new_val = old_val - given_val; - - break; - - case "*": - - new_val = old_val * given_val; - - break; - - case "/": - - new_val = old_val / given_val; - - break; - - } - - if(new_val != undefined && old_val != new_val) - - send_hmi_value(index, new_val); - - // TODO else raise - - return new_val; - - } - - - - var current_visible_page; - - var current_subscribed_page; - - var current_page_index; - - - - function prepare_svg() { - - for(let eltid in detachable_elements){ - - let [element,parent] = detachable_elements[eltid]; - - parent.removeChild(element); - - } - - }; - - - - function switch_page(page_name, page_index) { - - if(current_subscribed_page != current_visible_page){ - - /* page switch already going */ - - /* TODO LOG ERROR */ - - return false; - - } - - - - if(page_name == undefined) - - page_name = current_subscribed_page; - - - - - - let old_desc = page_desc[current_subscribed_page]; - - let new_desc = page_desc[page_name]; - - - - if(new_desc == undefined){ - - /* TODO LOG ERROR */ - - return false; - - } - - - - if(page_index == undefined){ - - page_index = new_desc.page_index; - - } - - - - if(old_desc){ - - old_desc.absolute_widgets.map(w=>w.unsub()); - - old_desc.relative_widgets.map(w=>w.unsub()); - - } - - new_desc.absolute_widgets.map(w=>w.sub()); - var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; - new_desc.relative_widgets.map(w=>w.sub(new_offset)); + new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness)); diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/svghmi.js --- a/svghmi/svghmi.js Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/svghmi.js Wed Aug 05 18:49:29 2020 +0200 @@ -323,12 +323,10 @@ } if(old_desc){ - old_desc.absolute_widgets.map(w=>w.unsub()); - old_desc.relative_widgets.map(w=>w.unsub()); - } - new_desc.absolute_widgets.map(w=>w.sub()); + old_desc.widgets.map(([widget,relativeness])=>widget.unsub()); + } var new_offset = page_index == undefined ? 0 : page_index - new_desc.page_index; - new_desc.relative_widgets.map(w=>w.sub(new_offset)); + new_desc.widgets.map(([widget,relativeness])=>widget.sub(new_offset,relativeness)); update_subscriptions(); diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/widget_foreach.ysl2 --- a/svghmi/widget_foreach.ysl2 Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/widget_foreach.ysl2 Wed Aug 05 18:49:29 2020 +0200 @@ -2,6 +2,9 @@ template "widget[@type='ForEach']", mode="widget_defs" { param "hmi_element"; + if "count(path) != 1" error > ForEach widget «$hmi_element/@id» must have one HMI path given. + if "count(arg) != 1" error > ForEach widget «$hmi_element/@id» must have one argument given : a class name. + const "class","arg[1]/@value"; const "base_path","path/@value"; @@ -49,34 +52,48 @@ template "widget[@type='ForEach']", mode="widget_class" || class ForEachWidget extends Widget{ - unsub(){ + + unsub_items(){ for(let item of this.items){ for(let widget of item) { widget.unsub(); } } - this.offset = 0; } - foreach_widgets_do(todo){ + unsub(){ + this.unsub_items(); + this.offset = 0; + this.relativeness = undefined; + } + + sub_items(){ for(let i = 0; i < this.items.length; i++) { let item = this.items[i]; let orig_item_index = this.index_pool[i]; let item_index = this.index_pool[i+this.item_offset]; let item_index_offset = item_index - orig_item_index; + if(this.relativeness[0]) + item_index_offset += this.offset; for(let widget of item) { - todo(widget).call(widget, this.offset + item_index_offset); + /* all variables of all widgets in a ForEach are all relative. + Really. + + TODO: allow absolute variables in ForEach widgets + */ + widget.sub(item_index_offset, widget.indexes.map(_=>true)); } } } - sub(new_offset=0){ + sub(new_offset=0, relativeness=[]){ this.offset = new_offset; - this.foreach_widgets_do(w=>w.sub); + this.relativeness = relativeness; + this.sub_items(); } apply_cache() { - this.foreach_widgets_do(w=>w.apply_cache); + this.items.forEach(item=>item.forEach(widget=>widget.apply_cache())); } on_click(opstr, evt) { @@ -93,8 +110,8 @@ new_item_offset = 0; } this.item_offset = new_item_offset; - this.unsub(); - this.sub(this.offset); + this.unsub_items(); + this.sub_items(); update_subscriptions(); need_cache_apply.push(this); jumps_need_update = true; diff -r 705e34c6fe93 -r ff9ae4f4e3be svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Tue Aug 04 11:53:39 2020 +0200 +++ b/svghmi/widgets_common.ysl2 Wed Aug 05 18:49:29 2020 +0200 @@ -78,20 +78,28 @@ unsub(){ /* remove subsribers */ - if(!this.unsubscribable) for(let index of this.indexes){ - let idx = index + this.offset; - subscribers[idx].delete(this); - } + if(!this.unsubscribable) + for(let i = 0; i < this.indexes.length; i++) { + let index = this.indexes[i]; + if(this.relativeness[i]) + index += this.offset; + subscribers[index].delete(this); + } this.offset = 0; - } - - sub(new_offset=0){ - /* set the offset because relative */ + this.relativeness = undefined; + } + + sub(new_offset=0, relativeness){ this.offset = new_offset; + this.relativeness = relativeness; /* add this's subsribers */ - if(!this.unsubscribable) for(let index of this.indexes){ - subscribers[index + new_offset].add(this); - } + if(!this.unsubscribable) + for(let i = 0; i < this.indexes.length; i++) { + let index = this.indexes[i]; + if(relativeness[i]) + index += new_offset; + subscribers[index].add(this); + } need_cache_apply.push(this); } @@ -107,7 +115,7 @@ get_idx(index) { let orig = this.indexes[index]; - return this.offset ? orig + this.offset : orig; + return this.relativeness[index] ? orig + this.offset : orig; } change_hmi_value(index,opstr) { return change_hmi_value(this.get_idx(index), opstr);