edouard@3241: // widget_foreach.ysl2 edouard@3241: edouard@3241: widget_desc("ForEach") { edouard@3241: edouard@3241: longdesc edouard@3241: || edouard@3241: ForEach widget is used to span a small set of widget over a larger set of edouard@3241: repeated HMI_NODEs. edouard@3241: edouard@3241: Idea is somewhat similar to relative page, but it all happens inside the edouard@3241: ForEach widget, no page involved. edouard@3241: edouard@3241: Together with relative Jump widgets it can be used to build a menu to reach edouard@3241: relative pages covering many identical HMI_NODES siblings. edouard@3241: edouard@3241: ForEach widget takes a HMI_CLASS name as argument and a HMI_NODE path as edouard@3241: variable. edouard@3241: edouard@3241: Direct sub-elements can be either groups of widget to be spanned, labeled edouard@3241: "ClassName:offset", or buttons to control the spanning, labeled edouard@3241: "ClassName:+/-number". edouard@4025: edouard@4025: In case of "ClassName:offset", offset for first element is 1. edouard@4025: edouard@3241: || edouard@3241: edouard@3241: shortdesc > span widgets over a set of repeated HMI_NODEs edouard@3241: edouard@3241: arg name="class_name" accepts="string" > HMI_CLASS name edouard@3241: edouard@3241: path name="root" accepts="HMI_NODE" > where to find HMI_NODEs whose HMI_CLASS is class_name edouard@4048: path name="position" accepts="HMI_INT" > position of HMI_NODE mapped to first item, among similar siblings edouard@4048: path name="range" accepts="HMI_INT" count="optional" > count of HMI_NODE siblings edouard@4048: path name="size" accepts="HMI_INT" count="optional" > count of visible items edouard@3241: } edouard@2891: edouard@3232: widget_defs("ForEach") { edouard@2894: edouard@4025: if "count(path) < 1" error > ForEach widget «$hmi_element/@id» must have one HMI path given. edouard@3005: if "count(arg) != 1" error > ForEach widget «$hmi_element/@id» must have one argument given : a class name. edouard@3005: edouard@2894: const "class","arg[1]/@value"; edouard@2894: edouard@2894: const "base_path","path/@value"; edouard@2894: const "hmi_index_base", "$indexed_hmitree/*[@hmipath = $base_path]"; edouard@2894: const "hmi_tree_base", "$hmitree/descendant-or-self::*[@path = $hmi_index_base/@path]"; edouard@2894: const "hmi_tree_items", "$hmi_tree_base/*[@class = $class]"; edouard@2894: const "hmi_index_items", "$indexed_hmitree/*[@path = $hmi_tree_items/@path]"; edouard@2894: const "items_paths", "$hmi_index_items/@hmipath"; Edouard@2893: | index_pool: [ edouard@2894: foreach "$hmi_index_items" { edouard@2894: | «@index»`if "position()!=last()" > ,` edouard@2894: } Edouard@2893: | ], edouard@2896: | init: function() { Edouard@2893: const "prefix","concat($class,':')"; Edouard@2893: const "buttons_regex","concat('^',$prefix,'[+\-][0-9]+')"; edouard@2896: const "buttons", "$hmi_element/*[regexp:test(@inkscape:label, $buttons_regex)]"; edouard@2896: foreach "$buttons" { edouard@2896: const "op","substring-after(@inkscape:label, $prefix)"; edouard@2896: | id("«@id»").setAttribute("onclick", "hmi_widgets['«$hmi_element/@id»'].on_click('«$op»', evt)"); Edouard@2893: } edouard@2896: | Edouard@2895: | this.items = [ Edouard@2893: const "items_regex","concat('^',$prefix,'[0-9]+')"; Edouard@2893: const "unordered_items","$hmi_element//*[regexp:test(@inkscape:label, $items_regex)]"; Edouard@2893: foreach "$unordered_items" { Edouard@2893: const "elt_label","concat($prefix, string(position()))"; Edouard@2893: const "elt","$unordered_items[@inkscape:label = $elt_label]"; edouard@2894: const "pos","position()"; edouard@2894: const "item_path", "$items_paths[$pos]"; Edouard@2895: | [ /* item="«$elt_label»" path="«$item_path»" */ Edouard@2893: if "count($elt)=0" error > Missing item labeled «$elt_label» in ForEach widget «$hmi_element/@id» edouard@4025: if "count($elt)>1" error > Duplicate item labeled «$elt_label» in ForEach widget «$hmi_element/@id» Edouard@2893: foreach "func:refered_elements($elt)[@id = $hmi_elements/@id][not(@id = $elt/@id)]" { edouard@2894: if "not(func:is_descendant_path(func:widget(@id)/path/@value, $item_path))" edouard@2896: error > Widget id="«@id»" label="«@inkscape:label»" is having wrong path. Accroding to ForEach widget ancestor id="«$hmi_element/@id»", path should be descendant of "«$item_path»". Edouard@2895: | hmi_widgets["«@id»"]`if "position()!=last()" > ,` edouard@2892: } Edouard@2895: | ]`if "position()!=last()" > ,` edouard@2892: } Edouard@2895: | ] Edouard@2895: | }, edouard@4025: | range: «count($hmi_index_items)», edouard@4025: | size: «count($unordered_items)», edouard@4025: | position: 0, edouard@2891: } edouard@2891: edouard@3232: widget_class("ForEach") edouard@2952: || edouard@4025: items_subscribed = false; edouard@3005: edouard@3005: unsub_items(){ edouard@4025: if(this.items_subscribed){ edouard@4025: for(let item of this.items){ edouard@4025: for(let widget of item) { edouard@4025: widget.unsub(); edouard@4025: } edouard@4025: } edouard@4025: this.items_subscribed = false; edouard@4025: } edouard@4025: } edouard@4025: edouard@4025: unsub(){ edouard@4025: super.unsub() edouard@4025: this.unsub_items(); edouard@4025: } edouard@4025: edouard@4025: sub_items(){ edouard@4025: if(!this.items_subscribed){ edouard@4025: for(let i = 0; i < this.size; i++) { edouard@4025: let item = this.items[i]; edouard@4025: let orig_item_index = this.index_pool[i]; edouard@4025: let item_index = this.index_pool[i+this.position]; edouard@4025: let item_index_offset = item_index - orig_item_index; edouard@4025: if(this.relativeness[0]) edouard@4025: item_index_offset += this.offset; edouard@4025: for(let widget of item) { edouard@4025: /* all variables of all widgets in a ForEach are all relative. edouard@4025: Really. edouard@4025: edouard@4025: TODO: allow absolute variables in ForEach widgets edouard@4025: */ edouard@4025: widget.sub(item_index_offset, widget.indexes.map(_=>true)); edouard@4025: } edouard@2952: } edouard@2952: } edouard@2952: } edouard@2891: edouard@4025: sub(new_offset, relativeness, container_id){ edouard@4048: let position_given = this.indexes.length > 1; edouard@4025: edouard@4025: // sub() will call apply_cache() and then dispatch() edouard@4025: // undefining position forces dispatch() to call apply_position() edouard@4025: if(position_given) edouard@4025: this.position = undefined; edouard@4025: edouard@4025: super.sub(new_offset, relativeness, container_id); edouard@4025: edouard@4025: // if position isn't given as a variable edouard@4025: // dispatch() to call apply_position() aren't called edouard@4025: // and items must be subscibed now. edouard@4025: if(!position_given) edouard@4025: this.sub_items(); edouard@4025: edouard@4025: // as soon as subribed apply range and size once for all edouard@4048: if(this.indexes.length > 2) edouard@4048: this.apply_hmi_value(2, this.range); edouard@4048: if(this.indexes.length > 3) edouard@4048: this.apply_hmi_value(3, this.size); edouard@3005: } edouard@3005: edouard@4025: apply_position(new_position){ edouard@4025: let old_position = this.position; edouard@4025: let limited_position = Math.round(Math.max(Math.min(new_position, this.range - this.size), 0)); edouard@4025: if(this.position == limited_position){ edouard@4025: return false; edouard@4025: } edouard@4025: this.unsub_items(); edouard@4025: this.position = limited_position; edouard@4025: this.sub_items(); edouard@4025: request_subscriptions_update(); edouard@4025: jumps_need_update = true; edouard@4025: this.request_animate(); edouard@4025: return true; edouard@4025: } edouard@3005: edouard@4025: on_click(opstr, evt) { edouard@4025: let new_position = eval(String(this.position)+opstr); edouard@4025: if(new_position + this.size > this.range) { edouard@4025: if(this.position + this.size == this.range) edouard@4025: new_position = 0; edouard@4025: else edouard@4025: new_position = this.range - this.size; edouard@4025: } else if(new_position < 0) { edouard@4025: if(this.position == 0) edouard@4025: new_position = this.range - this.size; edouard@4025: else edouard@4025: new_position = 0; edouard@4025: } edouard@4025: if(this.apply_position(new_position)){ edouard@4048: this.apply_hmi_value(1, this.position); edouard@4025: } edouard@4025: } edouard@4025: edouard@4025: dispatch(value, oldval, index) { edouard@4025: // Only care about position, others are constants edouard@4048: if(index == 1){ edouard@4025: this.apply_position(value); edouard@4025: if(this.position != value){ edouard@4025: // widget refused or apply different value, force it back edouard@4048: this.apply_hmi_value(1, this.position); edouard@2952: } edouard@2942: } edouard@2942: } edouard@2942: edouard@2952: || edouard@2942: