# HG changeset patch # User Edouard Tisserant # Date 1621322924 -7200 # Node ID f037e901a17c98492039f875f8e64ae2cb485b22 # Parent fe945f1f48b7e59f8b5993c047641980ad23a8f0# Parent 5f756332ada18914b9408bd0a2f1668f13e4e6a9 Merged SVGHMI branches diff -r 5f756332ada1 -r f037e901a17c svghmi/Makefile --- a/svghmi/Makefile Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/Makefile Tue May 18 09:28:44 2021 +0200 @@ -11,7 +11,7 @@ yml2path ?= $(abspath ../../yml2) -ysl2files := gen_index_xhtml.ysl2 gen_dnd_widget_svg.ysl2 +ysl2files := gen_index_xhtml.ysl2 gen_dnd_widget_svg.ysl2 analyse_widget.ysl2 ysl2includes := $(filter-out $(ysl2files), $(wildcard *.ysl2)) xsltfiles := $(patsubst %.ysl2, %.xslt, $(ysl2files)) diff -r 5f756332ada1 -r f037e901a17c svghmi/analyse_widget.xslt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/analyse_widget.xslt Tue May 18 09:28:44 2021 +0200 @@ -0,0 +1,673 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Widget id: + + label: + + has wrong syntax of path section + + + + + + + + + PAGE_LOCAL + + + + + HMI_LOCAL + + + + + + + + Widget id: + + label: + + path section + + use min and max on non mumeric value + + + + + + + + + + + + + + + + + + + + + : + + + + @ + + + , + + , + + + + + HMI: + + + + + + + + + + + AnimateRotation - DEPRECATED, do not use. + + Doesn't follow WYSIWYG principle, and forces user to add animateTransform tag in SVG (using inkscape XML editor for exemple) + + + + AnimateRotation - DEPRECATED + + + speed + + + + + + + + Back widget brings focus back to previous page in history when clicked. + + + + Jump to previous page + + + + + + + + Button widget takes one boolean variable path, and reflect current true + + or false value by showing "active" or "inactive" labeled element + + respectively. Pressing and releasing button changes variable to true and + + false respectively. Potential inconsistency caused by quick consecutive + + presses on the button is mitigated by using a state machine that wait for + + previous state change to be reflected on variable before applying next one. + + + + Push button reflecting consistently given boolean variable + + + Boolean variable + + + + + + + + CircularBar widget changes the end angle of a "path" labeled arc according + + to value of the single accepted variable. + + + + If "min" a "max" labeled texts are provided, then they are used as + + respective minimum and maximum value. Otherwise, value is expected to be + + in between 0 and 100. + + + + If "value" labeled text is found, then its content is replaced by value. + + + + Change end angle of Inkscape's arc + + + Value to display + + + + + + + + CircularSlider - DEPRECATED, to be replaced by PathSlider + + This widget moves "handle" labeled group along "range" labeled + + arc, according to value of the single accepted variable. + + + + If "min" a "max" labeled texts are provided, or if first and second + + argument are given, then they are used as respective minimum and maximum + + value. Otherwise, value is expected to be in between 0 and 100. + + + + If "value" labeled text is found, then its content is replaced by value. + + During drag, "setpoint" labeled group is moved to position defined by user + + while "handle" reflects current value from variable. + + + + CircularSlider - DEPRECATED + + + minimum value + + + maximum value + + + Value to display + + + + + + + + CustomHtml widget allows insertion of HTML code in a svg:foreignObject. + + Widget content is replaced by foreignObject. HTML code is obtained from + + "code" labeled text content. HTML insert position and size is given with + + "container" labeled element. + + + + Custom HTML insert + + + + + + + + If Display widget is a svg:text element, then text content is replaced by + + value of given variables, space separated. + + + + Otherwise, if Display widget is a group containing a svg:text element + + labelled "format", then text content is replaced by printf-like formated + + string. In other words, if "format" labeled text is "%d %s %f", then 3 + + variables paths are expected : HMI_IN, HMI_STRING and HMI_REAL. + + + + In case Display widget is a svg::text element, it is also possible to give + + format string as first argument. + + + + Printf-like formated text display + + + printf-like format string when not given as svg:text + + + variables to be displayed + + + + + + + + DropDown widget let user select an entry in a list of texts, given as + + arguments. Single variable path is index of selection. + + + + It needs "text" (svg:text), "box" (svg:rect), "button" (svg:*), + + and "highlight" (svg:rect) labeled elements. + + + + When user clicks on "button", "text" is duplicated to display enties in the + + limit of available space in page, and "box" is extended to contain all + + texts. "highlight" is moved over pre-selected entry. + + + + When only one argument is given, and argment contains "#langs" then list of + + texts is automatically set to the list of human-readable languages supported + + by this HMI. + + + + Let user select text entry in a drop-down menu + + + drop-down menu entries + + + selection index + + + + + + + + ForEach widget is used to span a small set of widget over a larger set of + + repeated HMI_NODEs. + + + + Idea is somewhat similar to relative page, but it all happens inside the + + ForEach widget, no page involved. + + + + Together with relative Jump widgets it can be used to build a menu to reach + + relative pages covering many identical HMI_NODES siblings. + + + + ForEach widget takes a HMI_CLASS name as argument and a HMI_NODE path as + + variable. + + + + Direct sub-elements can be either groups of widget to be spanned, labeled + + "ClassName:offset", or buttons to control the spanning, labeled + + "ClassName:+/-number". + + + + span widgets over a set of repeated HMI_NODEs + + + HMI_CLASS name + + + where to find HMI_NODEs whose HMI_CLASS is class_name + + + + + + + + Input widget takes one variable path, and displays current value in + + optional "value" labeled sub-element. + + + + Click on optional "edit" labeled element opens keypad to edit value. + + + + Operation on current value is performed when click on sub-elements with + + label starting with '=', '+' or '-' sign. Value after sign is used as + + operand. + + + + Input field with predefined operation buttons + + + optional printf-like format + + + single variable to edit + + + + + + + + Send given variables as POST to http URL argument, spread returned JSON in + + SVG sub-elements of "data" labeled element. + + + + Documentation to be written. see svbghmi exemple. + + + + Http POST variables, spread JSON back + + + + + + single variable to edit + + + + + + + + Jump widget brings focus to a different page. Mandatory single 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. + + + + "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. + + + + Jump to given page + + + name of page to jump to + + + reference for relative jump + + + + + + + + Keypad - to be written + + + + Keypad + + + keypad can input those types + + + + + + + + + + + + + Meter widget moves the end of "needle" labeled path along "range" labeled + + path, according to value of the single accepted variable. + + + + Needle is reduced to a single segment. If "min" a "max" labeled texts + + are provided, or if first and second argument are given, then they are used + + as respective minimum and maximum value. Otherwise, value is expected to be + + in between 0 and 100. + + + + If "value" labeled text is found, then its content is replaced by value. + + + + Moves "needle" along "range" + + + minimum value + + + maximum value + + + Value to display + + + + + + + + ScrollBar - documentation to be written + + + + ScrollBar + + + value + + + range + + + visible + + + + + + + + Slider - DEPRECATED - use ScrollBar or PathSlider instead + + + + Slider - DEPRECATED - use ScrollBar instead + + + value + + + range + + + visible + + + + + + + + Switch widget hides all subelements whose label do not match given + + variable current value representation. For exemple if given variable type + + is HMI_INT and value is 1, then elements with label '1' will be displayed. + + Label can have comments, so '1#some comment' would also match. If matching + + variable of type HMI_STRING, then double quotes must be used. For exemple, + + '"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + + + + Show elements whose label match value. + + + value to compare to labels + + + + + + + + Button widget takes one boolean variable path, and reflect current true + + or false value by showing "active" or "inactive" labeled element + + respectively. Clicking or touching button toggles variable. + + + + Toggle button reflecting given boolean variable + + + Boolean variable + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 5f756332ada1 -r f037e901a17c svghmi/analyse_widget.ysl2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svghmi/analyse_widget.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -0,0 +1,49 @@ +include yslt_noindent.yml2 + +in xsl decl widget_desc(%name, match="widget[@type='%name']", mode="widget_desc") alias template { + type > «@type» + content; +}; + +decl nothing alias - ; +decl widget_class(%name) alias - {nothing}; +decl widget_defs(%name) alias - {nothing}; +decl widget_page(%name) alias - {nothing}; +decl gen_index_xhtml alias - {nothing}; +decl emit(*name) alias - {nothing}; + +istylesheet + /* From Inkscape */ + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + + extension-element-prefixes="ns func exsl regexp str dyn" + exclude-result-prefixes="ns func exsl regexp str dyn svg inkscape" { + + const "indexed_hmitree", "/.."; // compatibility with parse_labels.ysl2 + include parse_labels.ysl2 + + const "hmi_elements", "//svg:*[starts-with(@inkscape:label, 'HMI:')]"; + + include widget_*.ysl2 + + template "@* | node()", mode="document" { + xsl:copy apply "@* | node()", mode="document"; + } + + template "widget", mode="document" { + xsl:copy { + apply "@* | node()", mode="document"; + defs apply ".", mode="widget_desc"; + } + } + + template "/" { + const "widgets" + apply "$hmi_elements", mode="parselabel"; + const "widget_ns", "exsl:node-set($widgets)"; + widgets + apply "$widget_ns", mode="document"; + } + +} diff -r 5f756332ada1 -r f037e901a17c svghmi/detachable_pages.ysl2 --- a/svghmi/detachable_pages.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/detachable_pages.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -193,8 +193,7 @@ | "«@id»": detachable_elements["«@id»"]`if "position()!=last()" > ,` } | } - /* TODO generate some code for init() instead */ - apply "$parsed_widgets/widget[@id = $all_page_widgets/@id]", mode="per_page_widget_template"{ + apply "$parsed_widgets/widget[@id = $all_page_widgets/@id]", mode="widget_page"{ with "page_desc", "$desc"; } | }`if "position()!=last()" > ,` @@ -208,7 +207,7 @@ | } } -template "*", mode="per_page_widget_template"; +template "*", mode="widget_page"; emit "debug:detachable-pages" { diff -r 5f756332ada1 -r f037e901a17c svghmi/gen_dnd_widget_svg.xslt --- a/svghmi/gen_dnd_widget_svg.xslt Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/gen_dnd_widget_svg.xslt Tue May 18 09:28:44 2021 +0200 @@ -1,11 +1,11 @@ - + - + @@ -51,22 +51,29 @@ - - + + + + + + + + + - + + + + - - - - + Widget id: @@ -128,9 +135,9 @@ @ - : + , - : + , @@ -214,7 +221,7 @@ - + diff -r 5f756332ada1 -r f037e901a17c svghmi/gen_dnd_widget_svg.ysl2 --- a/svghmi/gen_dnd_widget_svg.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/gen_dnd_widget_svg.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -11,7 +11,6 @@ xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:xhtml="http://www.w3.org/1999/xhtml" /* Namespace to invoke python code */ xmlns:ns="beremiz" @@ -20,7 +19,6 @@ exclude-result-prefixes="ns func exsl regexp str dyn" { param "hmi_path"; - const "svg", "/svg:svg"; const "hmi_elements", "//svg:*[starts-with(@inkscape:label, 'HMI:')]"; const "subhmitree", "ns:GetSubHMITree()"; @@ -84,7 +82,7 @@ msg value "$svg_widget_type"; } - value "ns:GiveDetails($testmsg)"; + value "ns:PassMessage($testmsg)"; apply "/", mode="inline_svg"; } diff -r 5f756332ada1 -r f037e901a17c svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/gen_index_xhtml.xslt Tue May 18 09:28:44 2021 +0200 @@ -136,6 +136,7 @@ + @@ -181,22 +182,29 @@ - - + + + + + + + + + - + + + + - - - - + Widget id: @@ -677,7 +685,7 @@ } - + } @@ -708,7 +716,7 @@ - + @@ -1870,8 +1878,10 @@ - - class AnimateWidget extends Widget{ + + class + AnimateWidget + extends Widget{ frequency = 5; @@ -1954,13 +1964,27 @@ } - - - - - - - class AnimateRotationWidget extends Widget{ + + + + + + AnimateRotation - DEPRECATED, do not use. + + Doesn't follow WYSIWYG principle, and forces user to add animateTransform tag in SVG (using inkscape XML editor for exemple) + + + + AnimateRotation - DEPRECATED + + + speed + + + + class + AnimateRotationWidget + extends Widget{ frequency = 5; @@ -1988,6 +2012,8 @@ // change animation properties + // TODO : rewrite with proper es6 + for(let child of this.element.children){ if(child.nodeName == "animateTransform"){ @@ -2039,13 +2065,22 @@ } - - - - - - - class BackWidget extends Widget{ + + + + + + Back widget brings focus back to previous page in history when clicked. + + + + Jump to previous page + + + + class + BackWidget + extends Widget{ on_click(evt) { @@ -2070,6 +2105,31 @@ } + + + + + + Button widget takes one boolean variable path, and reflect current true + + or false value by showing "active" or "inactive" labeled element + + respectively. Pressing and releasing button changes variable to true and + + false respectively. Potential inconsistency caused by quick consecutive + + presses on the button is mitigated by using a state machine that wait for + + previous state change to be reflected on variable before applying next one. + + + + Push button reflecting consistently given boolean variable + + + Boolean variable + + @@ -2218,10 +2278,12 @@ ); - + + class + ButtonWidget + extends Widget{ + - class ButtonWidget extends Widget{ - frequency = 5; display = "inactive"; @@ -2287,7 +2349,7 @@ } - + @@ -2297,8 +2359,39 @@ - - class CircularBarWidget extends Widget{ + + + + + + CircularBar widget changes the end angle of a "path" labeled arc according + + to value of the single accepted variable. + + + + If "min" a "max" labeled texts are provided, then they are used as + + respective minimum and maximum value. Otherwise, value is expected to be + + in between 0 and 100. + + + + If "value" labeled text is found, then its content is replaced by value. + + + + Change end angle of Inkscape's arc + + + Value to display + + + + class + CircularBarWidget + extends Widget{ frequency = 10; @@ -2395,7 +2488,7 @@ } - + @@ -2411,8 +2504,51 @@ - - class CircularSliderWidget extends Widget{ + + + + + + CircularSlider - DEPRECATED, to be replaced by PathSlider + + This widget moves "handle" labeled group along "range" labeled + + arc, according to value of the single accepted variable. + + + + If "min" a "max" labeled texts are provided, or if first and second + + argument are given, then they are used as respective minimum and maximum + + value. Otherwise, value is expected to be in between 0 and 100. + + + + If "value" labeled text is found, then its content is replaced by value. + + During drag, "setpoint" labeled group is moved to position defined by user + + while "handle" reflects current value from variable. + + + + CircularSlider - DEPRECATED + + + minimum value + + + maximum value + + + Value to display + + + + class + CircularSliderWidget + extends Widget{ frequency = 5; @@ -2867,7 +3003,7 @@ } - + @@ -2885,8 +3021,28 @@ - - class CustomHtmlWidget extends Widget{ + + + + + + CustomHtml widget allows insertion of HTML code in a svg:foreignObject. + + Widget content is replaced by foreignObject. HTML code is obtained from + + "code" labeled text content. HTML insert position and size is given with + + "container" labeled element. + + + + Custom HTML insert + + + + class + CustomHtmlWidget + extends Widget{ frequency = 5; @@ -2927,7 +3083,7 @@ } - + @@ -2935,11 +3091,47 @@ container code - - - - - class DisplayWidget extends Widget{ + + + + + + + If Display widget is a svg:text element, then text content is replaced by + + value of given variables, space separated. + + + + Otherwise, if Display widget is a group containing a svg:text element + + labelled "format", then text content is replaced by printf-like formated + + string. In other words, if "format" labeled text is "%d %s %f", then 3 + + variables paths are expected : HMI_IN, HMI_STRING and HMI_REAL. + + + + In case Display widget is a svg::text element, it is also possible to give + + format string as first argument. + + + + Printf-like formated text display + + + printf-like format string when not given as svg:text + + + variables to be displayed + + + + class + DisplayWidget + extends Widget{ frequency = 5; @@ -2954,7 +3146,7 @@ } - + @@ -3507,14 +3699,52 @@ - - function numb_event(e) { - - e.stopPropagation(); - - } - - class DropDownWidget extends Widget{ + + + + + + DropDown widget let user select an entry in a list of texts, given as + + arguments. Single variable path is index of selection. + + + + It needs "text" (svg:text), "box" (svg:rect), "button" (svg:*), + + and "highlight" (svg:rect) labeled elements. + + + + When user clicks on "button", "text" is duplicated to display enties in the + + limit of available space in page, and "box" is extended to contain all + + texts. "highlight" is moved over pre-selected entry. + + + + When only one argument is given, and argment contains "#langs" then list of + + texts is automatically set to the list of human-readable languages supported + + by this HMI. + + + + Let user select text entry in a drop-down menu + + + drop-down menu entries + + + selection index + + + + class + DropDownWidget + extends Widget{ dispatch(value) { @@ -3742,9 +3972,9 @@ // Stop hogging all click events - svg_root.removeEventListener("pointerdown", numb_event, true); - - svg_root.removeEventListener("pointerup", numb_event, true); + svg_root.removeEventListener("pointerdown", this.numb_event, true); + + svg_root.removeEventListener("pointerup", this.numb_event, true); svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); @@ -3978,6 +4208,12 @@ } + numb_event(e) { + + e.stopPropagation(); + + } + open(){ let length = this.content.length; @@ -4032,9 +4268,9 @@ // disable interaction with background - svg_root.addEventListener("pointerdown", numb_event, true); - - svg_root.addEventListener("pointerup", numb_event, true); + svg_root.addEventListener("pointerdown", this.numb_event, true); + + svg_root.addEventListener("pointerup", this.numb_event, true); svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); @@ -4162,10 +4398,10 @@ } - } - - - + } + + + @@ -4193,7 +4429,53 @@ , - + + + + + + ForEach widget is used to span a small set of widget over a larger set of + + repeated HMI_NODEs. + + + + Idea is somewhat similar to relative page, but it all happens inside the + + ForEach widget, no page involved. + + + + Together with relative Jump widgets it can be used to build a menu to reach + + relative pages covering many identical HMI_NODES siblings. + + + + ForEach widget takes a HMI_CLASS name as argument and a HMI_NODE path as + + variable. + + + + Direct sub-elements can be either groups of widget to be spanned, labeled + + "ClassName:offset", or buttons to control the spanning, labeled + + "ClassName:+/-number". + + + + span widgets over a set of repeated HMI_NODEs + + + HMI_CLASS name + + + where to find HMI_NODEs whose HMI_CLASS is class_name + + + @@ -4307,8 +4589,10 @@ item_offset: 0, - - class ForEachWidget extends Widget{ + + class + ForEachWidget + extends Widget{ @@ -4443,73 +4727,103 @@ } - - class InputWidget extends Widget{ - - on_op_click(opstr) { - - this.change_hmi_value(0, opstr); - - } - - edit_callback(new_val) { - - this.apply_hmi_value(0, new_val); - - } - - - - is_inhibited = false; - - alert(msg){ - - this.is_inhibited = true; - - this.display = msg; - - setTimeout(() => this.stopalert(), 1000); - - this.request_animate(); - - } - - - - stopalert(){ - - this.is_inhibited = false; - - this.display = this.last_value; - - this.request_animate(); - - } - - - - overshot(new_val, max) { - - this.alert("max"); - - } - - - - undershot(new_val, min) { - - this.alert("min"); - - } - - - - - - } - - - + + + + + + Input widget takes one variable path, and displays current value in + + optional "value" labeled sub-element. + + + + Click on optional "edit" labeled element opens keypad to edit value. + + + + Operation on current value is performed when click on sub-elements with + + label starting with '=', '+' or '-' sign. Value after sign is used as + + operand. + + + + Input field with predefined operation buttons + + + optional printf-like format + + + single variable to edit + + + + class + InputWidget + extends Widget{ + + on_op_click(opstr) { + + this.change_hmi_value(0, opstr); + + } + + edit_callback(new_val) { + + this.apply_hmi_value(0, new_val); + + } + + + + is_inhibited = false; + + alert(msg){ + + this.is_inhibited = true; + + this.display = msg; + + setTimeout(() => this.stopalert(), 1000); + + this.request_animate(); + + } + + + + stopalert(){ + + this.is_inhibited = false; + + this.display = this.last_value; + + this.request_animate(); + + } + + + + overshot(new_val, max) { + + this.alert("max"); + + } + + + + undershot(new_val, min) { + + this.alert("min"); + + } + + } + + + @@ -4598,8 +4912,34 @@ }, - - class JsonTableWidget extends Widget{ + + + + + + Send given variables as POST to http URL argument, spread returned JSON in + + SVG sub-elements of "data" labeled element. + + + + Documentation to be written. see svbghmi exemple. + + + + Http POST variables, spread JSON back + + + + + + single variable to edit + + + + class + JsonTableWidget + extends Widget{ // arbitrary defaults to avoid missing entries in query @@ -5018,7 +5358,7 @@ } - + @@ -5064,10 +5404,48 @@ } - - class JumpWidget extends Widget{ - - + + + + + + Jump widget brings focus to a different page. Mandatory single 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. + + + + "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. + + + + Jump to given page + + + name of page to jump to + + + reference for relative jump + + + + class + JumpWidget + extends Widget{ activable = false; @@ -5193,10 +5571,10 @@ } - } - - - + } + + + @@ -5249,7 +5627,8 @@ }, - + + @@ -5316,6 +5695,21 @@ + + + + + + Keypad - to be written + + + + Keypad + + + keypad can input those types + + @@ -5351,10 +5745,10 @@ - - class KeypadWidget extends Widget{ - - + + class + KeypadWidget + extends Widget{ on_key_click(symbols) { @@ -5545,7 +5939,7 @@ } - + @@ -5606,7 +6000,12 @@ ], - + + + + + + items: { @@ -5621,7 +6020,10 @@ }, - + + + + styles: { @@ -5637,8 +6039,47 @@ }, - - class MeterWidget extends Widget{ + + + + + + Meter widget moves the end of "needle" labeled path along "range" labeled + + path, according to value of the single accepted variable. + + + + Needle is reduced to a single segment. If "min" a "max" labeled texts + + are provided, or if first and second argument are given, then they are used + + as respective minimum and maximum value. Otherwise, value is expected to be + + in between 0 and 100. + + + + If "value" labeled text is found, then its content is replaced by value. + + + + Moves "needle" along "range" + + + minimum value + + + maximum value + + + Value to display + + + + class + MetterWidget + extends Widget{ frequency = 10; @@ -5692,12 +6133,10 @@ } - - } - + @@ -5713,8 +6152,39 @@ - - class MultiStateWidget extends Widget{ + + + + Mutlistateh widget hides all subelements whose label do not match given + + variable value representation. For exemple if given variable type + + is HMI_INT and value is 1, then elements with label '1' will be displayed. + + Label can have comments, so '1#some comment' would also match. If matching + + variable of type HMI_STRING, then double quotes must be used. For exemple, + + '"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + + + + Click on widget changes variable value to next value in given list, or to + + first one if not initialized to value already part of the list. + + + + Show elements whose label match value. + + + value to compare to labels + + + + class + MultiStateWidget + extends Widget{ frequency = 5; @@ -5795,7 +6265,7 @@ } - + choices: [ @@ -5826,8 +6296,31 @@ ], - - class ScrollBarWidget extends Widget{ + + + + + + ScrollBar - documentation to be written + + + + ScrollBar + + + value + + + range + + + visible + + + + class + ScrollBarWidget + extends Widget{ frequency = 10; @@ -6004,7 +6497,7 @@ } - + @@ -6036,7 +6529,32 @@ }, - + + + + + + Slider - DEPRECATED - use ScrollBar or PathSlider instead + + + + Slider - DEPRECATED - use ScrollBar instead + + + value + + + range + + + visible + + + + class + SliderWidget + extends Widget{ + class SliderWidget extends Widget{ frequency = 5; @@ -6707,8 +7225,10 @@ } - - + } + + + @@ -6723,11 +7243,36 @@ - - - - - class SwitchWidget extends Widget{ + + + + + + + Switch widget hides all subelements whose label do not match given + + variable current value representation. For exemple if given variable type + + is HMI_INT and value is 1, then elements with label '1' will be displayed. + + Label can have comments, so '1#some comment' would also match. If matching + + variable of type HMI_STRING, then double quotes must be used. For exemple, + + '"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + + + + Show elements whose label match value. + + + value to compare to labels + + + + class + SwitchWidget + extends Widget{ frequency = 5; @@ -6752,7 +7297,7 @@ } - + choices: [ @@ -6786,8 +7331,29 @@ ], - - class ToggleButtonWidget extends Widget{ + + + + + + Button widget takes one boolean variable path, and reflect current true + + or false value by showing "active" or "inactive" labeled element + + respectively. Clicking or touching button toggles variable. + + + + Toggle button reflecting given boolean variable + + + Boolean variable + + + + class + ToggleButtonWidget + extends Widget{ frequency = 5; @@ -6866,7 +7432,7 @@ } - + diff -r 5f756332ada1 -r f037e901a17c svghmi/parse_labels.ysl2 --- a/svghmi/parse_labels.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/parse_labels.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -13,7 +13,10 @@ // path value="path4" index="path4" type="HMI_LOCAL"; // } // -template "*", mode="parselabel" { +const "pathregex",!"'^([^\[,]+)(\[[^\]]+\])?([\d,]*)$'"!; + +template "*", mode="parselabel" +{ const "label","@inkscape:label"; const "id","@id"; const "description", "substring-after($label,'HMI:')"; @@ -41,16 +44,24 @@ const "paths", "substring-after($description,'@')"; foreach "str:split($paths, '@')" { if "string-length(.) > 0" path { - const "pathminmax", "str:split(.,',')"; - const "path", "$pathminmax[1]"; + // 1 : global match + // 2 : /path + // 3 : [accepts] + // 4 : min,max + const "path_match", "regexp:match(.,$pathregex)"; + const "pathminmax", "str:split($path_match[4],',')"; + const "path", "$path_match[2]"; + const "path_accepts", "$path_match[3]"; const "pathminmaxcount", "count($pathminmax)"; attrib "value" > «$path» + if "string-length($path_accepts)" + attrib "accepts" > «$path_accepts» choose { - when "$pathminmaxcount = 3" { - attrib "min" > «$pathminmax[2]» - attrib "max" > «$pathminmax[3]» + when "$pathminmaxcount = 2" { + attrib "min" > «$pathminmax[1]» + attrib "max" > «$pathminmax[2]» } - when "$pathminmaxcount = 2" { + when "$pathminmaxcount = 1 or $pathminmaxcount > 2" { error > Widget id:«$id» label:«$label» has wrong syntax of path section «$pathminmax» } } diff -r 5f756332ada1 -r f037e901a17c svghmi/ui.py --- a/svghmi/ui.py Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/ui.py Tue May 18 09:28:44 2021 +0200 @@ -128,6 +128,7 @@ _conf_key = "SVGHMIWidgetLib" _preview_height = 200 +_preview_margin = 5 class WidgetLibBrowser(wx.Panel): def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize): @@ -142,22 +143,24 @@ self.Config = wx.ConfigBase.Get() self.libdir = self.RecallLibDir() - sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=4, vgap=0) - sizer.AddGrowableCol(0) - sizer.AddGrowableRow(1) + self.main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=5, vgap=0) + self.main_sizer.AddGrowableCol(0) + self.main_sizer.AddGrowableRow(1) self.libbutton = wx.Button(self, -1, _("Select SVG widget library")) self.widgetpicker = WidgetPicker(self, self.libdir) - self.preview = wx.Panel(self, size=(-1, _preview_height + 10)) - self.comment = wx.TextCtrl(self, size=wx.Size(-1, 80), + self.preview = wx.Panel(self, size=(-1, _preview_height + _preview_margin*2)) + self.desc = wx.TextCtrl(self, size=wx.Size(-1, 160), style=wx.TE_READONLY | wx.TE_MULTILINE) - sizer.AddWindow(self.libbutton, flag=wx.GROW) - sizer.AddWindow(self.widgetpicker, flag=wx.GROW) - sizer.AddWindow(self.preview, flag=wx.GROW) - sizer.AddWindow(self.comment, flag=wx.GROW) - sizer.Layout() + self.signature_sizer = wx.BoxSizer(wx.VERTICAL) + self.main_sizer.Add(self.libbutton, flag=wx.GROW) + self.main_sizer.Add(self.widgetpicker, flag=wx.GROW) + self.main_sizer.Add(self.preview, flag=wx.GROW) + self.main_sizer.Add(self.desc, flag=wx.GROW) + self.main_sizer.Add(self.signature_sizer, flag=wx.GROW) + self.main_sizer.Layout() self.SetAutoLayout(True) - self.SetSizer(sizer) - sizer.Fit(self) + self.SetSizer(self.main_sizer) + self.main_sizer.Fit(self) self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton) self.preview.Bind(wx.EVT_PAINT, self.OnPaint) self.preview.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) @@ -167,6 +170,21 @@ self.msg = _("Drag selected Widget from here to Inkscape") self.tempf = None + self.paths_editors = [] + + def ResetSignature(self): + self.signature_sizer.Clear() + for editor in self.paths_editors: + editor.Destroy() + self.paths_editors = [] + self.main_sizer.Layout() + + def AddPathToSignature(self, path): + new_editor = wx.TextCtrl(self, size=wx.Size(-1, -1)) + self.paths_editors.append(new_editor) + self.signature_sizer.Add(new_editor, flag=wx.GROW) + self.main_sizer.Layout() + def RecallLibDir(self): conf = self.Config.Read(_conf_key) if len(conf) == 0: @@ -191,9 +209,9 @@ # Get Preview panel size sz = self.preview.GetClientSize() w = self.bmp.GetWidth() - dc.DrawBitmap(self.bmp, (sz.width - w)/2, 5) - - self.comment.SetValue(self.msg) + dc.DrawBitmap(self.bmp, (sz.width - w)/2, _preview_margin) + + self.desc.SetValue(self.msg) def OnSelectLibDir(self, event): @@ -268,7 +286,9 @@ self.bmp = wx.Bitmap(thumbpath) if have_thumb else None self.selected_SVG = svgpath if have_thumb else None - self.ValidateWidget() + + self.AnalyseWidgetAndUpdateUI() + except IOError: self.msg = _("Widget library must be writable") @@ -293,9 +313,65 @@ for msg in msgs: self.msg += msg.text + "\n" + def PassMessage(self, _context, msgs): + for msg in msgs: + self.msg += msg.text + "\n" + def GetSubHMITree(self, _context): return [self.hmitree_node.etree()] + def AnalyseWidgetAndUpdateUI(self): + self.msg = "" + + try: + if self.selected_SVG is None: + raise Exception(_("No widget selected")) + + transform = XSLTransform( + os.path.join(ScriptDirectory, "analyse_widget.xslt"),[]) + + svgdom = etree.parse(self.selected_SVG) + + signature = transform.transform(svgdom) + + for entry in transform.get_error_log(): + self.msg += "XSLT: " + entry.message + "\n" + + except Exception as e: + self.msg += str(e) + except XSLTApplyError as e: + self.msg += "Widget analysis error: " + e.message + else: + + self.ResetSignature() + + print(etree.tostring(signature, pretty_print=True)) + widgets = signature.getroot() + for defs in widgets.iter("defs"): + + # Keep double newlines (to mark paragraphs) + self.msg += defs.find("type").text + ":\n" + "\n\n".join(map( + lambda s:s.replace("\n"," ").replace(" ", " "), + defs.find("longdesc").text.split("\n\n"))) + for arg in defs.iter("arg"): + print(arg.get("name")) + print(arg.get("accepts")) + for path in defs.iter("path"): + self.AddPathToSignature(path) + print(path.get("name")) + print(path.get("accepts")) + + for widget in widgets: + widget_type = widget.get("type") + print(widget_type) + for path in widget.iterchildren("path"): + path_value = path.get("value") + path_accepts = map( + str.strip, path.get("accepts", '')[1:-1].split(',')) + print(path, path_value, path_accepts) + + + def ValidateWidget(self): self.msg = "" @@ -312,7 +388,7 @@ transform = XSLTransform( os.path.join(ScriptDirectory, "gen_dnd_widget_svg.xslt"), [("GetSubHMITree", self.GetSubHMITree), - ("GiveDetails", self.GiveDetails)]) + ("PassMessage", self.GiveDetails)]) svgdom = etree.parse(self.selected_SVG) diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_animate.ysl2 --- a/svghmi/widget_animate.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_animate.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,7 @@ // widget_animate.ysl2 -template "widget[@type='Animate']", mode="widget_class"{ +widget_class("Animate") { || - class AnimateWidget extends Widget{ frequency = 5; speed = 0; start = false; @@ -42,12 +41,6 @@ let widget_pos = this.element.getBBox(); this.widget_center = [(widget_pos.x+widget_pos.width/2), (widget_pos.y+widget_pos.height/2)]; } - } || } - -template "widget[@type='Animate']", mode="widget_defs" { - param "hmi_element"; - |, -} diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_animaterotation.ysl2 --- a/svghmi/widget_animaterotation.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_animaterotation.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,20 @@ // widget_animaterotation.ysl2 -template "widget[@type='AnimateRotation']", mode="widget_class"{ +widget_desc("AnimateRotation") { + longdesc || - class AnimateRotationWidget extends Widget{ + AnimateRotation - DEPRECATED, do not use. + Doesn't follow WYSIWYG principle, and forces user to add animateTransform tag in SVG (using inkscape XML editor for exemple) + || + + shortdesc > AnimateRotation - DEPRECATED + + path name="speed" accepts="HMI_INT,HMI_REAL" > speed + +} + +widget_class("AnimateRotation") { + || frequency = 5; speed = 0; widget_center = undefined; @@ -16,6 +28,7 @@ animate(){ // change animation properties + // TODO : rewrite with proper es6 for(let child of this.element.children){ if(child.nodeName == "animateTransform"){ if(this.speed > 0){ @@ -40,12 +53,6 @@ let widget_pos = this.element.getBBox(); this.widget_center = [(widget_pos.x+widget_pos.width/2), (widget_pos.y+widget_pos.height/2)]; } - } || } - -template "widget[@type='AnimateRotation']", mode="widget_defs" { - param "hmi_element"; - |, -} diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_back.ysl2 --- a/svghmi/widget_back.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_back.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,17 @@ // widget_back.ysl2 -template "widget[@type='Back']", mode="widget_class" +widget_desc("Back") { + longdesc || - class BackWidget extends Widget{ + Back widget brings focus back to previous page in history when clicked. + || + + shortdesc > Jump to previous page +} + +// TODO: use es6 +widget_class("Back") + || on_click(evt) { if(jump_history.length > 1){ jump_history.pop(); @@ -13,5 +22,4 @@ init() { this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); } - } || diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_button.ysl2 --- a/svghmi/widget_button.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_button.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,5 +1,22 @@ // widget_button.ysl2 +widget_desc("Button") { + longdesc + || + Button widget takes one boolean variable path, and reflect current true + or false value by showing "active" or "inactive" labeled element + respectively. Pressing and releasing button changes variable to true and + false respectively. Potential inconsistency caused by quick consecutive + presses on the button is mitigated by using a state machine that wait for + previous state change to be reflected on variable before applying next one. + || + + shortdesc > Push button reflecting consistently given boolean variable + + path name="value" accepts="HMI_BOOL" > Boolean variable + +} + // Finite state machine decl fsm(name); decl state(name); @@ -9,6 +26,8 @@ decl show(eltname); decl hmi_value(value); +gen_index_xhtml { + // State machine to drive HMI_BOOL on a potentially laggy connection const "_button_fsm" fsm { state "init" { @@ -104,9 +123,10 @@ | this.apply_hmi_value(0, «@value»); } -template "widget[@type='Button']", mode="widget_class"{ +} + +widget_class("Button"){ const "fsm","exsl:node-set($_button_fsm)"; - | class ButtonWidget extends Widget{ | frequency = 5; | display = "inactive"; @@ -145,11 +165,8 @@ | this.bound_onmouseup = this.onmouseup.bind(this); | this.element.addEventListener("pointerdown", this.onmousedown.bind(this)); | } - | } } - -template "widget[@type='Button']", mode="widget_defs" { - param "hmi_element"; +widget_defs("Button") { optional_labels("active inactive"); } diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_circularbar.ysl2 --- a/svghmi/widget_circularbar.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_circularbar.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,28 @@ // widget_circularbar.ysl2 -template "widget[@type='CircularBar']", mode="widget_class"{ +widget_desc("CircularBar") { + longdesc || - class CircularBarWidget extends Widget{ + CircularBar widget changes the end angle of a "path" labeled arc according + to value of the single accepted variable. + + If "min" a "max" labeled texts are provided, then they are used as + respective minimum and maximum value. Otherwise, value is expected to be + in between 0 and 100. + + If "value" labeled text is found, then its content is replaced by value. + || + + shortdesc > Change end angle of Inkscape's arc + + // TODO: add min/max arguments + // TODO: add printf-like format + + path name="value" accepts="HMI_INT,HMI_REAL" > Value to display + +} +widget_class("CircularBar") { + || frequency = 10; range = undefined; @@ -49,12 +69,10 @@ this.center = [cx, cy]; this.proportions = [rx, ry]; } - } || } -template "widget[@type='CircularBar']", mode="widget_defs" { - param "hmi_element"; +widget_defs("CircularBar") { labels("path"); optional_labels("value min max"); } diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_circularslider.ysl2 --- a/svghmi/widget_circularslider.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_circularslider.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,35 @@ // widget_circuralslider.ysl2 -template "widget[@type='CircularSlider']", mode="widget_class" - || - class CircularSliderWidget extends Widget{ +widget_desc("CircularSlider") { + longdesc + || + CircularSlider - DEPRECATED, to be replaced by PathSlider + This widget moves "handle" labeled group along "range" labeled + arc, according to value of the single accepted variable. + + If "min" a "max" labeled texts are provided, or if first and second + argument are given, then they are used as respective minimum and maximum + value. Otherwise, value is expected to be in between 0 and 100. + + If "value" labeled text is found, then its content is replaced by value. + During drag, "setpoint" labeled group is moved to position defined by user + while "handle" reflects current value from variable. + || + + shortdesc > CircularSlider - DEPRECATED + + arg name="min" count="optional" accepts="int,real" > minimum value + + arg name="min" count="optional" accepts="int,real" > maximum value + + // TODO: add printf-like format + + path name="value" accepts="HMI_INT,HMI_REAL" > Value to display + +} + +widget_class("CircularSlider") + || frequency = 5; range = undefined; circle = undefined; @@ -228,11 +255,9 @@ } } - } - || - -template "widget[@type='CircularSlider']", mode="widget_defs" { - param "hmi_element"; + || + +widget_defs("CircularSlider") { labels("handle range"); optional_labels("value min max setpoint"); |, diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_customhtml.ysl2 --- a/svghmi/widget_customhtml.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_customhtml.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,21 @@ // widget_customhtml.ysl2 -template "widget[@type='CustomHtml']", mode="widget_class"{ +widget_desc("CustomHtml") { + longdesc || - class CustomHtmlWidget extends Widget{ + CustomHtml widget allows insertion of HTML code in a svg:foreignObject. + Widget content is replaced by foreignObject. HTML code is obtained from + "code" labeled text content. HTML insert position and size is given with + "container" labeled element. + || + + shortdesc > Custom HTML insert + + // TODO: support reload and POST based on variable content +} + +widget_class("CustomHtml"){ + || frequency = 5; widget_size = undefined; @@ -21,13 +34,10 @@ this.code_elt.textContent+ ' '; } - } || } -template "widget[@type='CustomHtml']", mode="widget_defs" { - param "hmi_element"; +widget_defs("CustomHtml") { labels("container code"); - |, } diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_display.ysl2 --- a/svghmi/widget_display.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_display.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,20 +1,39 @@ // widget_display.ysl2 - -template "widget[@type='Display']", mode="widget_class" +widget_desc("Display") { + longdesc || - class DisplayWidget extends Widget{ + If Display widget is a svg:text element, then text content is replaced by + value of given variables, space separated. + + Otherwise, if Display widget is a group containing a svg:text element + labelled "format", then text content is replaced by printf-like formated + string. In other words, if "format" labeled text is "%d %s %f", then 3 + variables paths are expected : HMI_IN, HMI_STRING and HMI_REAL. + + In case Display widget is a svg::text element, it is also possible to give + format string as first argument. + || + + shortdesc > Printf-like formated text display + + arg name="format" count="optional" accepts="string" > printf-like format string when not given as svg:text + + path name="fields" count="many" accepts="HMI_INT,HMI_REAL,HMI_STRING,HMI_BOOL" > variables to be displayed + +} + + +widget_class("Display") + || frequency = 5; dispatch(value, oldval, index) { this.fields[index] = value; this.request_animate(); } - } || -template "widget[@type='Display']", mode="widget_defs" { - param "hmi_element"; - +widget_defs("Display") { const "format" optional_labels("format"); const "has_format","string-length($format)>0"; value "$format"; diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_dropdown.ysl2 --- a/svghmi/widget_dropdown.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_dropdown.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,11 +1,35 @@ // widget_dropdown.ysl2 -template "widget[@type='DropDown']", mode="widget_class"{ +widget_desc("DropDown") { + + longdesc + || + DropDown widget let user select an entry in a list of texts, given as + arguments. Single variable path is index of selection. + + It needs "text" (svg:text), "box" (svg:rect), "button" (svg:*), + and "highlight" (svg:rect) labeled elements. + + When user clicks on "button", "text" is duplicated to display enties in the + limit of available space in page, and "box" is extended to contain all + texts. "highlight" is moved over pre-selected entry. + + When only one argument is given, and argment contains "#langs" then list of + texts is automatically set to the list of human-readable languages supported + by this HMI. + || + + shortdesc > Let user select text entry in a drop-down menu + + arg name="entries" count="many" accepts="string" > drop-down menu entries + + path name="selection" accepts="HMI_INT" > selection index +} + +// TODO: support i18n of menu entries using svg:text elements with labels starting with "_" + +widget_class("DropDown") { || - function numb_event(e) { - e.stopPropagation(); - } - class DropDownWidget extends Widget{ dispatch(value) { if(!this.opened) this.set_selection(value); } @@ -119,8 +143,8 @@ } close(){ // Stop hogging all click events - svg_root.removeEventListener("pointerdown", numb_event, true); - svg_root.removeEventListener("pointerup", numb_event, true); + svg_root.removeEventListener("pointerdown", this.numb_event, true); + svg_root.removeEventListener("pointerup", this.numb_event, true); svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); // Restore position and sixe of widget elements this.reset_text(); @@ -237,6 +261,9 @@ c++; } } + numb_event(e) { + e.stopPropagation(); + } open(){ let length = this.content.length; // systematically reset text, to strip eventual whitespace spans @@ -264,8 +291,8 @@ // Rise widget to top by moving it to last position among siblings this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); // disable interaction with background - svg_root.addEventListener("pointerdown", numb_event, true); - svg_root.addEventListener("pointerup", numb_event, true); + svg_root.addEventListener("pointerdown", this.numb_event, true); + svg_root.addEventListener("pointerup", this.numb_event, true); svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); this.highlight_selection(); @@ -329,12 +356,10 @@ // b.width.baseVal.value = 2 * lmargin + m.width; b.height.baseVal.value = 2 * tmargin + m.height; } - } || } -template "widget[@type='DropDown']", mode="widget_defs" { - param "hmi_element"; +widget_defs("DropDown") { labels("text box button highlight"); // It is assumed that list content conforms to Array interface. > content: diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_foreach.ysl2 --- a/svghmi/widget_foreach.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_foreach.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,6 +1,34 @@ +// widget_foreach.ysl2 -template "widget[@type='ForEach']", mode="widget_defs" { - param "hmi_element"; +widget_desc("ForEach") { + + longdesc + || + ForEach widget is used to span a small set of widget over a larger set of + repeated HMI_NODEs. + + Idea is somewhat similar to relative page, but it all happens inside the + ForEach widget, no page involved. + + Together with relative Jump widgets it can be used to build a menu to reach + relative pages covering many identical HMI_NODES siblings. + + ForEach widget takes a HMI_CLASS name as argument and a HMI_NODE path as + variable. + + Direct sub-elements can be either groups of widget to be spanned, labeled + "ClassName:offset", or buttons to control the spanning, labeled + "ClassName:+/-number". + || + + shortdesc > span widgets over a set of repeated HMI_NODEs + + arg name="class_name" accepts="string" > HMI_CLASS name + + path name="root" accepts="HMI_NODE" > where to find HMI_NODEs whose HMI_CLASS is class_name +} + +widget_defs("ForEach") { 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. @@ -49,9 +77,8 @@ | item_offset: 0, } -template "widget[@type='ForEach']", mode="widget_class" +widget_class("ForEach") || -class ForEachWidget extends Widget{ unsub_items(){ for(let item of this.items){ @@ -117,6 +144,5 @@ jumps_need_update = true; requestHMIAnimation(); } -} || diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_input.ysl2 --- a/svghmi/widget_input.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_input.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,44 +1,59 @@ // widget_input.ysl2 -template "widget[@type='Input']", mode="widget_class"{ -|| - class InputWidget extends Widget{ - on_op_click(opstr) { - this.change_hmi_value(0, opstr); - } - edit_callback(new_val) { - this.apply_hmi_value(0, new_val); - } +widget_desc("Input") { + longdesc + || + Input widget takes one variable path, and displays current value in + optional "value" labeled sub-element. - is_inhibited = false; - alert(msg){ - this.is_inhibited = true; - this.display = msg; - setTimeout(() => this.stopalert(), 1000); - this.request_animate(); - } + Click on optional "edit" labeled element opens keypad to edit value. + + Operation on current value is performed when click on sub-elements with + label starting with '=', '+' or '-' sign. Value after sign is used as + operand. + || - stopalert(){ - this.is_inhibited = false; - this.display = this.last_value; - this.request_animate(); - } + shortdesc > Input field with predefined operation buttons - overshot(new_val, max) { - this.alert("max"); - } + arg name="format" accepts="string" > optional printf-like format - undershot(new_val, min) { - this.alert("min"); - } - - - } -|| + path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING" > single variable to edit + } -template "widget[@type='Input']", mode="widget_defs" { - param "hmi_element"; +widget_class("Input") +|| + on_op_click(opstr) { + this.change_hmi_value(0, opstr); + } + edit_callback(new_val) { + this.apply_hmi_value(0, new_val); + } + + is_inhibited = false; + alert(msg){ + this.is_inhibited = true; + this.display = msg; + setTimeout(() => this.stopalert(), 1000); + this.request_animate(); + } + + stopalert(){ + this.is_inhibited = false; + this.display = this.last_value; + this.request_animate(); + } + + overshot(new_val, max) { + this.alert("max"); + } + + undershot(new_val, min) { + this.alert("min"); + } +|| + +widget_defs("Input") { const "value_elt" optional_labels("value"); const "have_value","string-length($value_elt)>0"; diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_jsontable.ysl2 --- a/svghmi/widget_jsontable.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_jsontable.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,24 @@ // widget_jsontable.ysl2 -template "widget[@type='JsonTable']", mode="widget_class" +widget_desc("JsonTable") { + longdesc + || + Send given variables as POST to http URL argument, spread returned JSON in + SVG sub-elements of "data" labeled element. + + Documentation to be written. see svbghmi exemple. || - class JsonTableWidget extends Widget{ + + shortdesc > Http POST variables, spread JSON back + + arg name="url" accepts="string" > + + path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING" > single variable to edit + +} + +widget_class("JsonTable") + || // arbitrary defaults to avoid missing entries in query cache = [0,0,0]; init_common() { @@ -83,9 +99,10 @@ // on_click(evt, ...options) { // this.do_http_request(...options); // } - } || +gen_index_xhtml { + template "svg:*", mode="json_table_elt_render" { error > JsonTable Widget can't contain element of type «local-name()». } @@ -259,8 +276,9 @@ | } } -template "widget[@type='JsonTable']", mode="widget_defs" { - param "hmi_element"; +} + +widget_defs("JsonTable") { labels("data"); const "data_elt", "$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']"; | visible: «count($data_elt/*[@inkscape:label])», diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_jump.ysl2 --- a/svghmi/widget_jump.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_jump.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,9 +1,30 @@ // widget_jump.ysl2 -template "widget[@type='Jump']", mode="widget_class"{ +widget_desc("Jump") { + longdesc + || + Jump widget brings focus to a different page. Mandatory single 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. + + "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. + || + + shortdesc > Jump to given page + + arg name="page" accepts="string" > name of page to jump to + + path name="reference" count="optional" accepts="HMI_NODE" > reference for relative jump +} + +widget_class("Jump") { || - class JumpWidget extends Widget{ - activable = false; active = false; disabled = false; @@ -66,12 +87,11 @@ this.disabled = !Number(value); this.update_state(); } - } || } -template "widget[@type='Jump']", mode="widget_defs" { - param "hmi_element"; +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"; @@ -103,7 +123,7 @@ } -template "widget[@type='Jump']", mode="per_page_widget_template"{ +widget_page("Jump"){ param "page_desc"; /* check that given path is compatible with page's reference path */ if "path" { diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_keypad.ysl2 --- a/svghmi/widget_keypad.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_keypad.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,5 +1,17 @@ // widget_keypad.ysl2 +widget_desc("Keypad") { + longdesc + || + Keypad - to be written + || + + shortdesc > Keypad + + arg name="supported_types" accepts="string" > keypad can input those types + +} + emit "declarations:keypad" { | | var keypads = { @@ -13,10 +25,8 @@ | } } -template "widget[@type='Keypad']", mode="widget_class" +widget_class("Keypad") || - class KeypadWidget extends Widget{ - on_key_click(symbols) { var syms = symbols.split(" "); this.shift |= this.caps; @@ -110,11 +120,9 @@ (this.caps?this.activate_activable:this.inactivate_activable)(this.CapsLock_sub); } } - } || -template "widget[@type='Keypad']", mode="widget_defs" { - param "hmi_element"; +widget_defs("Keypad") { labels("Esc Enter BackSpace Keys Info Value"); optional_labels("Sign Space NumDot"); activable_labels("CapsLock Shift"); diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_list.ysl2 --- a/svghmi/widget_list.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_list.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,7 +1,9 @@ // widget_list.ysl2 +widget_desc("List") { + // TODO +} -template "widget[@type='List']", mode="widget_defs" { - param "hmi_element"; +widget_defs("List") { | items: { foreach "$hmi_element/*[@inkscape:label]" { | «@inkscape:label»: "«@id»", @@ -9,8 +11,11 @@ | }, } -template "widget[@type='TextStyleList']", mode="widget_defs" { - param "hmi_element"; +widget_defs("TextStyleList") { + // TODO +} + +widget_defs("TextStyleList") { | styles: { foreach "$hmi_element/*[@inkscape:label]" { const "style", "func:refered_elements(.)[self::svg:text]/@style"; diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_meter.ysl2 --- a/svghmi/widget_meter.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_meter.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,33 @@ // widget_meter.ysl2 -template "widget[@type='Meter']", mode="widget_class"{ +widget_desc("Meter") { + longdesc || - class MeterWidget extends Widget{ + Meter widget moves the end of "needle" labeled path along "range" labeled + path, according to value of the single accepted variable. + + Needle is reduced to a single segment. If "min" a "max" labeled texts + are provided, or if first and second argument are given, then they are used + as respective minimum and maximum value. Otherwise, value is expected to be + in between 0 and 100. + + If "value" labeled text is found, then its content is replaced by value. + || + + shortdesc > Moves "needle" along "range" + + arg name="min" count="optional" accepts="int,real" > minimum value + + arg name="max" count="optional" accepts="int,real" > maximum value + + // TODO: add printf-like format + + path name="value" accepts="HMI_INT,HMI_REAL" > Value to display + +} + +widget_class("Metter"){ + || frequency = 10; origin = undefined; range = undefined; @@ -29,13 +54,10 @@ this.range = [min, max, this.range_elt.getTotalLength()] this.origin = this.needle_elt.getPointAtLength(0); } - - } || } -template "widget[@type='Meter']", mode="widget_defs" { - param "hmi_element"; +widget_defs("Meter") { labels("needle range"); optional_labels("value min max"); } diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_multistate.ysl2 --- a/svghmi/widget_multistate.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_multistate.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,30 @@ // widget_multistate.ysl2 -template "widget[@type='MultiState']", mode="widget_class" +widget_defs("MultiState") { + + longdesc || - class MultiStateWidget extends Widget{ + Mutlistateh widget hides all subelements whose label do not match given + variable value representation. For exemple if given variable type + is HMI_INT and value is 1, then elements with label '1' will be displayed. + Label can have comments, so '1#some comment' would also match. If matching + variable of type HMI_STRING, then double quotes must be used. For exemple, + '"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + + Click on widget changes variable value to next value in given list, or to + first one if not initialized to value already part of the list. + || + + shortdesc > Show elements whose label match value. + + // TODO: add optional format/precision argument to support floating points + + path name="value" accepts="HMI_INT,HMI_STRING" > value to compare to labels + +} + +widget_class("MultiState") + || frequency = 5; state = 0; dispatch(value) { @@ -41,11 +63,9 @@ init() { this.element.setAttribute("onclick", "hmi_widgets['"+this.element_id+"'].on_click(evt)"); } - } || -template "widget[@type='MultiState']", mode="widget_defs" { - param "hmi_element"; +widget_defs("MultiState") { | choices: [ const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+|false|true)(#.*)?$'"!; foreach "$result_svg_ns//*[@id = $hmi_element/@id]//*[regexp:test(@inkscape:label,$regex)]" { diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_scrollbar.ysl2 --- a/svghmi/widget_scrollbar.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_scrollbar.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,20 @@ // widget_scrollbar.ysl2 +widget_desc("ScrollBar") { + longdesc + || + ScrollBar - documentation to be written + || -template "widget[@type='ScrollBar']", mode="widget_class"{ + shortdesc > ScrollBar + + path name="value" accepts="HMI_INT" > value + path name="range" accepts="HMI_INT" > range + path name="visible" accepts="HMI_INT" > visible + +} + +widget_class("ScrollBar") { || - class ScrollBarWidget extends Widget{ frequency = 10; position = undefined; range = undefined; @@ -89,12 +101,10 @@ this.dragpos += movement * units / pixels; this.apply_position(this.dragpos); } - } || } -template "widget[@type='ScrollBar']", mode="widget_defs" { - param "hmi_element"; +widget_defs("ScrollBar") { labels("cursor range"); const "pagebuttons" optional_labels("pageup pagedown"); diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_slider.ysl2 --- a/svghmi/widget_slider.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_slider.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,6 +1,20 @@ // widget_slider.ysl2 -template "widget[@type='Slider']", mode="widget_class" +widget_desc("Slider") { + longdesc + || + Slider - DEPRECATED - use ScrollBar or PathSlider instead + || + + shortdesc > Slider - DEPRECATED - use ScrollBar instead + + path name="value" accepts="HMI_INT" > value + path name="range" accepts="HMI_INT" > range + path name="visible" accepts="HMI_INT" > visible + +} + +widget_class("Slider") || class SliderWidget extends Widget{ frequency = 5; @@ -339,9 +353,7 @@ } || -template "widget[@type='Slider']", mode="widget_defs" { - param "hmi_element"; +widget_defs("Slider") { labels("handle range"); optional_labels("value min max setpoint"); - |, } diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_switch.ysl2 --- a/svghmi/widget_switch.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_switch.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,8 +1,27 @@ // widget_switch.ysl2 -template "widget[@type='Switch']", mode="widget_class" +widget_desc("Switch") { + longdesc || - class SwitchWidget extends Widget{ + Switch widget hides all subelements whose label do not match given + variable current value representation. For exemple if given variable type + is HMI_INT and value is 1, then elements with label '1' will be displayed. + Label can have comments, so '1#some comment' would also match. If matching + variable of type HMI_STRING, then double quotes must be used. For exemple, + '"hello"' or '"hello"#another comment' match HMI_STRING 'hello'. + || + + shortdesc > Show elements whose label match value. + + // TODO: add optional format/precision argument to support floating points + // TODO: support (in)equations and ranges + + path name="value" accepts="HMI_INT,HMI_STRING" > value to compare to labels + +} + +widget_class("Switch") + || frequency = 5; dispatch(value) { for(let choice of this.choices){ @@ -13,11 +32,9 @@ } } } - } || -template "widget[@type='Switch']", mode="widget_defs" { - param "hmi_element"; +widget_defs("Switch") { | choices: [ const "regex",!"'^(\"[^\"].*\"|\-?[0-9]+|false|true)(#.*)?$'"!; @@ -29,6 +46,7 @@ const "literal", "regexp:match(@inkscape:label,$regex)[2]"; | { | elt:id("«@id»"), + // TODO : use style.display = "none" to hide element | style:"«@style»", | value:«$literal» | }`if "position()!=last()" > ,` diff -r 5f756332ada1 -r f037e901a17c svghmi/widget_tooglebutton.ysl2 --- a/svghmi/widget_tooglebutton.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widget_tooglebutton.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -1,9 +1,22 @@ // widget_tooglebutton.ysl2 -template "widget[@type='ToggleButton']", mode="widget_class"{ +widget_desc("ToggleButton") { + longdesc || - class ToggleButtonWidget extends Widget{ + Button widget takes one boolean variable path, and reflect current true + or false value by showing "active" or "inactive" labeled element + respectively. Clicking or touching button toggles variable. + || + + shortdesc > Toggle button reflecting given boolean variable + + path name="value" accepts="HMI_BOOL" > Boolean variable + +} + +widget_class("ToggleButton") { + || frequency = 5; state = 0; active_style = undefined; @@ -41,11 +54,9 @@ this.activate(false); this.element.onclick = (evt) => this.on_click(evt); } - } || } -template "widget[@type='ToggleButton']", mode="widget_defs" { - param "hmi_element"; +widget_defs("ToggleButton") { optional_labels("active inactive"); } diff -r 5f756332ada1 -r f037e901a17c svghmi/widgetlib/modern_knob_1.svg --- a/svghmi/widgetlib/modern_knob_1.svg Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widgetlib/modern_knob_1.svg Tue May 18 09:28:44 2021 +0200 @@ -159,16 +159,16 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="0.35" - inkscape:cx="41.428571" - inkscape:cy="555.71429" + inkscape:zoom="1.979899" + inkscape:cx="208.80035" + inkscape:cy="841.9769" inkscape:document-units="mm" - inkscape:current-layer="svg2637" + inkscape:current-layer="g3058" showgrid="false" - inkscape:window-width="1414" - inkscape:window-height="840" - inkscape:window-x="1690" - inkscape:window-y="117" + inkscape:window-width="1623" + inkscape:window-height="1446" + inkscape:window-x="3346" + inkscape:window-y="244" inkscape:window-maximized="0" /> @@ -178,7 +178,7 @@ image/svg+xml - + @@ -206,7 +206,7 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff -r 5f756332ada1 -r f037e901a17c svghmi/widgets_common.ysl2 --- a/svghmi/widgets_common.ysl2 Mon May 17 08:52:38 2021 +0200 +++ b/svghmi/widgets_common.ysl2 Tue May 18 09:28:44 2021 +0200 @@ -21,6 +21,31 @@ } }; +in xsl decl widget_desc(%name, match="widget[@type='%name']", mode="widget_desc") alias template { + type > «@type» + content; +}; + +in xsl decl widget_class(%name, *clsname="%nameWidget", match="widget[@type='%name']", mode="widget_class") alias template { + | class `text **clsname` extends Widget{ + content; + | } +}; + +in xsl decl widget_defs(%name, match="widget[@type='%name']", mode="widget_defs") alias template { + param "hmi_element"; + content; +}; + +in xsl decl widget_page(%name, match="widget[@type='%name']", mode="widget_page") alias template { + param "page_desc"; + content; +}; + +decl gen_index_xhtml alias - { + content; +}; + template "svg:*", mode="hmi_widgets" { const "widget", "func:widget(@id)"; const "eltid","@id";