--- a/LPCSVGHMI/Makefile Wed May 06 16:27:02 2026 +0200
+++ b/LPCSVGHMI/Makefile Tue May 12 14:06:42 2026 +0200
@@ -1,5 +1,11 @@
+# yml2c resolves includes by merging EVERY matching file on YML_PATH (see yml2/backend.py). +# gen_index_xhtml.ysl2 uses "include detachable_pages.ysl2", so both this directory and +# beremiz/svghmi would be concatenated if stock detachable_pages.ysl2 stayed in place. +# Before compiling, we temporarily hide beremiz's copy so only LPCSVGHMI/detachable_pages.ysl2 our_lib_path = $(abspath .)
original_svghmi_path = $(abspath ../../beremiz/svghmi)
@@ -7,10 +13,14 @@
xsltfiles := gen_index_xhtml.xslt gen_dnd_widget_svg.xslt analyse_widget.xslt
+STOCK_DETACHABLE := $(original_svghmi_path)/detachable_pages.ysl2 +HIDDEN_DETACHABLE := $(original_svghmi_path)/detachable_pages.ysl2._lpc_build_hidden $(xsltfiles): $(our_widgets)
- for f in $(xsltfiles); do rm -f $(original_svghmi_path)/$$f; done
- YML_PATH=$(original_svghmi_path):$(our_lib_path) $(MAKE) -C $(original_svghmi_path)
+ for f in $(xsltfiles); do rm -f $(original_svghmi_path)/$$f; done + @test -f '$(STOCK_DETACHABLE)' && mv '$(STOCK_DETACHABLE)' '$(HIDDEN_DETACHABLE)' || true + @YML_PATH=$(our_lib_path):$(original_svghmi_path) $(MAKE) -C $(original_svghmi_path) \ + || { test -f '$(HIDDEN_DETACHABLE)' && mv '$(HIDDEN_DETACHABLE)' '$(STOCK_DETACHABLE)'; exit 1; }; \ + test -f '$(HIDDEN_DETACHABLE)' && mv '$(HIDDEN_DETACHABLE)' '$(STOCK_DETACHABLE)'; exit 0 for f in $(xsltfiles); do cp $(original_svghmi_path)/$$f $(our_lib_path)/$$f; done
--- a/LPCSVGHMI/analyse_widget.xslt Wed May 06 16:27:02 2026 +0200
+++ b/LPCSVGHMI/analyse_widget.xslt Tue May 12 14:06:42 2026 +0200
@@ -245,6 +245,304 @@
<xsl:apply-templates mode="genlabel" select="path"/>
<xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/>
+ <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label + <xsl:text>HMI:AnimateRotation:optional_args + <xsl:text>Element to rotate is a part of that group labeled "animate". + <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right", + <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element + <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used. + <xsl:text>Optional arguments are: + <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set) + <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set) + <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used) + <xsl:text>The higher the frame rate, the higher CPU usage will be. + <xsl:text>Rotation animation</xsl:text> + <xsl:template match="widget[@type='CloudImage']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by + <xsl:text>link to the file whose name is the value of given variable, being served from + <xsl:text>/media/data/cloud folder. + <xsl:text>Image display</xsl:text> + <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>DropDownIndexed widget can have one, two or three path variables. + <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text), + <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect) + <xsl:text>labeled elements. + <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the + <xsl:text>limit of available space in page, and "box" is extended to contain all + <xsl:text>"highlight" is moved over pre-selected entry. + <xsl:text>The first variable path is index of selection, and the second is value of selection. + <xsl:text>In case there are one or two path variables, a list of texts is defined via + <xsl:text>If there are no arguments, it is expected that "text" labeled element is of + <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget. + <xsl:text>In that case list of texts is set to TextList content. + <xsl:text>When only one argument is given and its value is "#langs" then list of + <xsl:text>texts is automatically set to the human-readable list of supported + <xsl:text>languages by this HMI. + <xsl:text>Otherwise, arguments are used as dropdown options. + <xsl:text>In case there are three path variables, the third path variable is a filter + <xsl:text>in a form of a string containing ':' separated list of indices of the options + <xsl:text>from the arguments that will be shown in the dropdown. + <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE + <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER + <xsl:text>Let user select text entry in a drop-down menu</xsl:text> + <arg name="entries" count="many" accepts="string"> + <xsl:text>drop-down menu entries</xsl:text> + <path name="selected_index" accepts="HMI_INT"> + <xsl:text>selection index</xsl:text> + <path name="selected_value" accepts="HMI_STRING"> + <xsl:text>selection value</xsl:text> + <path name="filter" accepts="HMI_STRING"> + <xsl:text>indices of shown drop-down menu entries</xsl:text> + <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis, + <xsl:text>grid/marks, legends and curves. + <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing: + <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels. + <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be + <xsl:text> duplicated along axis line to form intervals marks. + <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their + <xsl:text> bounding box is the chart wall. + <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used + <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths. + <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored. + <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text> + <path name="value" count="1+" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <arg name="xformat" count="optional" accepts="string"> + <xsl:text>format string for X label</xsl:text> + <arg name="yformat" count="optional" accepts="string"> + <xsl:text>format string for Y label</xsl:text> + <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in + <xsl:text>SVG sub-elements of "data" labeled element. + <xsl:text>Documentation to be written. see svghmi example. + <xsl:text>Http POST variables, spread JSON back</xsl:text> + <arg name="url" accepts="string"/> + <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING"> + <xsl:text>single variable to edit</xsl:text> + <xsl:template match="widget[@type='Swipe']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes + <xsl:text>associated actions. The widget should be placed on top of the area where the + <xsl:text>movement should be detected. It is a group containing a graphical element + <xsl:text>"area" which defines the area where the swipe should be detected. + <xsl:text>For each of the motions to be detected there must exist several parameters + <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set: + <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents + <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of + <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to + <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set + <xsl:text>to). change and set commands should also be accompanied by paths with the + <xsl:text>same name and their values should be variable names to apply the command to. + <xsl:text>Additional parameters to add are: + <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer + <xsl:text> movement. Anything below that value will not be considered + <xsl:text> a movement. If omitted, 5 will be used. + <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer + <xsl:text> down event. If time elapses without any significant movement + <xsl:text> (defined by movethreshold), the pointer down/click event + <xsl:text> will be propagated on an element on a lower level than the + <xsl:text> swiping area. Similar thing will happen on pointer up event + <xsl:text> if there was no significant movement. + <xsl:text>HMI:Swipe:movethreshold=3:left_xthreshold=30:left_ythreshold=5:left_jump=Home:up_xthreshold=25:up_ythreshold=5:up_change=+2@up_change=/VAR0 + <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must + <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget + <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect + <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and + <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2. + <xsl:text>Detect swipe motion and react accordingly</xsl:text> + <xsl:template match="widget[@type='TouchDetect']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>TouchDetect publishes whether any finger is on the screen via one HMI variable (first path). + <xsl:text>Place the group under the document svg as a sibling of page groups (it is not tied to any page's + <xsl:text>widget list). init() sets relativeness/offset so get_variable_index works without calling sub() + <xsl:text>(sub() registers with subscribers and runs apply_cache; not needed for outbound-only touch writes). + <xsl:text>Label example: HMI:TouchDetect@/YourBoolOrInt — while at least one touch pointer is active the + <xsl:text>variable is 1, otherwise 0. Listeners are on document.body (pointerdown/pointerup/pointercancel). + <xsl:text>Global touch active flag (body pointer events, touch pointers only)</xsl:text> + <path name="touch_active" count="1" accepts="HMI_BOOL,HMI_INT"/> <xsl:template match="widget[@type='Assign']" mode="widget_desc">
<xsl:value-of select="@type"/>
@@ -1110,281 +1408,6 @@
<func:result select="$res"/>
- <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label
- <xsl:text>HMI:AnimateRotation:optional_args
- <xsl:text>Element to rotate is a part of that group labeled "animate".
- <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right",
- <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element
- <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used.
- <xsl:text>Optional arguments are:
- <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set)
- <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set)
- <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used)
- <xsl:text>The higher the frame rate, the higher CPU usage will be.
- <xsl:text>Rotation animation</xsl:text>
- <xsl:template match="widget[@type='CloudImage']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by
- <xsl:text>link to the file whose name is the value of given variable, being served from
- <xsl:text>/media/data/cloud folder.
- <xsl:text>Image display</xsl:text>
- <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>DropDownIndexed widget can have one, two or three path variables.
- <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text),
- <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect)
- <xsl:text>labeled elements.
- <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the
- <xsl:text>limit of available space in page, and "box" is extended to contain all
- <xsl:text>"highlight" is moved over pre-selected entry.
- <xsl:text>The first variable path is index of selection, and the second is value of selection.
- <xsl:text>In case there are one or two path variables, a list of texts is defined via
- <xsl:text>If there are no arguments, it is expected that "text" labeled element is of
- <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget.
- <xsl:text>In that case list of texts is set to TextList content.
- <xsl:text>When only one argument is given and its value is "#langs" then list of
- <xsl:text>texts is automatically set to the human-readable list of supported
- <xsl:text>languages by this HMI.
- <xsl:text>Otherwise, arguments are used as dropdown options.
- <xsl:text>In case there are three path variables, the third path variable is a filter
- <xsl:text>in a form of a string containing ':' separated list of indices of the options
- <xsl:text>from the arguments that will be shown in the dropdown.
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER
- <xsl:text>Let user select text entry in a drop-down menu</xsl:text>
- <arg name="entries" count="many" accepts="string">
- <xsl:text>drop-down menu entries</xsl:text>
- <path name="selected_index" accepts="HMI_INT">
- <xsl:text>selection index</xsl:text>
- <path name="selected_value" accepts="HMI_STRING">
- <xsl:text>selection value</xsl:text>
- <path name="filter" accepts="HMI_STRING">
- <xsl:text>indices of shown drop-down menu entries</xsl:text>
- <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis,
- <xsl:text>grid/marks, legends and curves.
- <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing:
- <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels.
- <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be
- <xsl:text> duplicated along axis line to form intervals marks.
- <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their
- <xsl:text> bounding box is the chart wall.
- <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used
- <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths.
- <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored.
- <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text>
- <path name="value" count="1+" accepts="HMI_INT,HMI_REAL">
- <xsl:text>value</xsl:text>
- <arg name="xformat" count="optional" accepts="string">
- <xsl:text>format string for X label</xsl:text>
- <arg name="yformat" count="optional" accepts="string">
- <xsl:text>format string for Y label</xsl:text>
- <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in
- <xsl:text>SVG sub-elements of "data" labeled element.
- <xsl:text>Documentation to be written. see svghmi example.
- <xsl:text>Http POST variables, spread JSON back</xsl:text>
- <arg name="url" accepts="string"/>
- <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING">
- <xsl:text>single variable to edit</xsl:text>
- <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes
- <xsl:text>associated actions. The widget should be placed on top of the area where the
- <xsl:text>movement should be detected. It is a group containing a graphical element
- <xsl:text>"area" which defines the area where the swipe should be detected.
- <xsl:text>For each of the motions to be detected there must exist several parameters
- <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set:
- <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents
- <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of
- <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to
- <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set
- <xsl:text>to). change and set commands should also be accompanied by paths with the
- <xsl:text>same name and their values should be variable names to apply the command to.
- <xsl:text>Additional parameters to add are:
- <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer
- <xsl:text> movement. Anything below that value will not be considered
- <xsl:text> a movement. If omitted, 5 will be used.
- <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer
- <xsl:text> down event. If time elapses without any significant movement
- <xsl:text> (defined by movethreshold), the pointer down/click event
- <xsl:text> will be propagated on an element on a lower level than the
- <xsl:text> swiping area. Similar thing will happen on pointer up event
- <xsl:text> if there was no significant movement.
- <xsl:text>HMI:Swipe:movethreshold=3:left_xthreshold=30:left_ythreshold=5:left_jump=Home:up_xthreshold=25:up_ythreshold=5:up_change=+2@up_change=/VAR0
- <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must
- <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget
- <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect
- <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and
- <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2.
- <xsl:text>Detect swipe motion and react accordingly</xsl:text>
<xsl:template mode="document" match="@* | node()">
<xsl:apply-templates mode="document" select="@* | node()"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LPCSVGHMI/detachable_pages.ysl2 Tue May 12 14:06:42 2026 +0200
@@ -0,0 +1,260 @@
+// detachable_pages.ysl2 (LPC) +// Fork of beremiz/svghmi/detachable_pages.ysl2: TouchDetect widgets are always required +// (never discarded) and never detached with pages. +// yml2 merges every matching include from YML_PATH; Makefile hides the stock +// detachable_pages.ysl2 during the svghmi build so this file is the only match. +// compute what elements are required by pages +// and decide where to cut when removing/attaching +// pages elements on page switch +const "hmi_pages_descs", "$parsed_widgets/widget[@type = 'Page']"; +const "hmi_pages", "$hmi_elements[@id = $hmi_pages_descs/@id]"; +const "default_page" choose { + when "count($hmi_pages) > 1" { + when "$hmi_pages_descs/arg[1]/@value = 'Home'" > Home + error > No Home page defined! + when "count($hmi_pages) = 0" { + error > No page defined! + otherwise > «func:widget($hmi_pages/@id)/arg[1]/@value» +emit "preamble:default-page" { + | var default_page = "«$default_page»"; + const "screensaverpage", "$hmi_pages_descs[arg[1]/@value = 'ScreenSaver']"; + when "$screensaverpage" { + const "delaystr", "$screensaverpage/arg[2]/@value"; + if "not(regexp:test($delaystr,'^[0-9]+$'))" + error > ScreenSaver page has missing or malformed delay argument. + | var screensaver_delay = «$delay»; +const "keypads_descs", "$parsed_widgets/widget[@type = 'Keypad']"; +const "keypads", "$hmi_elements[@id = $keypads_descs/@id]"; +const "touchdetect_widget_ids", "$parsed_widgets/widget[@type = 'TouchDetect']/@id"; +const "touchdetect_elements", "$hmi_elements[@id = $touchdetect_widget_ids]/descendant-or-self::svg:*"; +// returns all directly or indirectly refered elements +def "func:refered_elements" { + const "descend", "$elems/descendant-or-self::svg:*"; + const "clones", "$descend[self::svg:use]"; + const "originals", "//svg:*[concat('#',@id) = $clones/@xlink:href]"; + result "$descend | func:refered_elements($originals)"; +// variable "overlapping_geometry" was added for optimization. +// It avoids calling func:overlapping_geometry 3 times for each page +// (apparently libxml doesn't cache exslt function results) +// in order to optimize further, func:overlapping_geometry +// should be implemented in python or even C, +// as this is still the main bottleneck here +const "_overlapping_geometry" { + foreach "$hmi_pages | $keypads" { + const "k", "concat('overlapping:', @id)"; + value "ns:ProgressStart($k, concat('collecting membership of ', @inkscape:label))"; + copy "func:overlapping_geometry(.)"; + value "ns:ProgressEnd($k)"; +const "overlapping_geometry", "exsl:node-set($_overlapping_geometry)"; +def "func:all_related_elements" { + const "page_overlapping_geometry", "$overlapping_geometry/elt[@id = $page/@id]/*"; + const "overlapping_candidates", "//svg:*[not(starts-with((ancestor::svg:g | .) /@inkscape:label, 'DISCARD:'))]"; + const "page_overlapping_elements", "$overlapping_candidates[@id = $page_overlapping_geometry/@Id]"; + const "page_widgets_elements", """ + $hmi_elements[not(@id=$page/@id) + and descendant-or-self::svg:*/@id = $page_overlapping_elements/@id] + /descendant-or-self::svg:*"""; + const "page_sub_elements", "func:refered_elements($page | $page_overlapping_elements | $page_widgets_elements)"; + result "$page_sub_elements"; +def "func:required_elements" { + result """func:all_related_elements($pages[1]) + | func:required_elements($pages[position()!=1])"""; +const "required_page_elements", + "func:required_elements($hmi_pages | $keypads)/ancestor-or-self::svg:*"; +const "required_list_elements", "func:refered_elements(($hmi_lists | $hmi_textlists)[@id = $required_page_elements/@id])/ancestor-or-self::svg:*"; +const "required_elements", "$defs | $required_list_elements | $required_page_elements | $touchdetect_elements"; +const "discardable_elements", "//svg:*[not(@id = $required_elements/@id)]"; +def "func:sumarized_elements" { + const "short_list", "$elements[not(ancestor::*/@id = $elements/@id)]"; + const "filled_groups", """$short_list/parent::svg:g[ + not(@id = $discardable_elements/@id) and + not(@id = $short_list/@id) + const "groups_to_add", "$filled_groups[not(ancestor::*/@id = $filled_groups/@id)]"; + result "$groups_to_add | $short_list[not(ancestor::*/@id = $filled_groups/@id)]"; +def "func:detachable_elements" { + result """func:sumarized_elements(func:all_related_elements($pages[1])) + | func:detachable_elements($pages[position()!=1])"""; +// Avoid nested detachables +const "_detachable_elements", "func:detachable_elements($hmi_pages | $keypads)"; +const "detachable_elements", "$_detachable_elements[not(ancestor::*/@id = $_detachable_elements/@id) and not(@id = $touchdetect_widget_ids)]"; +emit "declarations:page-class" { + | class PageWidget extends Widget{} +emit "declarations:detachable-elements" { + | var detachable_elements = { + foreach "$detachable_elements"{ + | "«@id»":[id("«@id»"), id("«../@id»")]`if "position()!=last()" > ,` +const "forEach_widgets_ids", "$parsed_widgets/widget[@type = 'ForEach']/@id"; +const "forEach_widgets", "$hmi_widgets[@id = $forEach_widgets_ids]"; +const "in_forEach_widget_ids", "func:refered_elements($forEach_widgets)[not(@id = $forEach_widgets_ids)]/@id"; +template "svg:*", mode="page_desc" { + if "ancestor::*[@id = $hmi_pages/@id]" error > HMI:Page «@id» is nested in another HMI:Page + const "desc", "func:widget(@id)"; + const "pagename", "$desc/arg[1]/@value"; + const "msg", "concat('generating page description ', $pagename)"; + value "ns:ProgressStart($pagename, $msg)"; + const "p", "$geometry[@Id = $page/@id]"; + const "page_all_elements", "func:all_related_elements($page)"; + const "all_page_widgets","$hmi_widgets[@id = $page_all_elements/@id and @id != $page/@id]"; + const "page_managed_widgets","$all_page_widgets[not(@id=$in_forEach_widget_ids)]"; + const "page_root_path", "$desc/path[not(@assign)]"; + if "count($page_root_path)>1" + error > Page id="«$page/@id»" : only one root path can be declared + const "page_relative_widgets", + "$page_managed_widgets[func:is_descendant_path(func:widget(@id)/path/@value, $page_root_path/@value)]"; + // Take closest ancestor in detachable_elements + // since nested detachable elements are filtered out + const "sumarized_page", + """func:sumarized_elements($page_all_elements)"""; + const "required_detachables", + ancestor-or-self::*[@id = $detachable_elements/@id]"""; + | bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»], + if "count($page_root_path)=1"{ + if "count($page_root_path/@index)=0" + warning > Page id="«$page/@id»" : No match for path "«$page_root_path/@value»" in HMI tree + | page_index: «$page_root_path/@index», + | page_class: "«$indexed_hmitree/*[@hmipath = $page_root_path/@value]/@class»", + | [hmi_widgets["«$page/@id»"], []], + foreach "$page_managed_widgets" { + const "widget_paths_relativeness" + foreach "func:widget(@id)/path" { + value "func:is_descendant_path(@value, $page_root_path/@value)"; + if "position()!=last()" > , + | [hmi_widgets["«@id»"], [«$widget_paths_relativeness»]]`if "position()!=last()" > ,` + foreach "$parsed_widgets/widget[@id = $all_page_widgets/@id and @type='Jump']" { + | hmi_widgets["«@id»"]`if "position()!=last()" > ,` + | required_detachables: { + foreach "$required_detachables" { + | "«@id»": detachable_elements["«@id»"]`if "position()!=last()" > ,` + apply "$parsed_widgets/widget[@id = $all_page_widgets/@id]", mode="widget_page"{ + with "page_desc", "$desc"; + | }`if "position()!=last()" > ,` + value "ns:ProgressEnd($pagename)"; +emit "definitions:page-desc" { + apply "$hmi_pages", mode="page_desc"; +template "*", mode="widget_page"; +emit "debug:detachable-pages" { + foreach "$detachable_elements"{ + foreach "$discardable_elements"{ + foreach "$in_forEach_widget_ids"{ + apply "$overlapping_geometry", mode="testtree"; --- a/LPCSVGHMI/gen_index_xhtml.xslt Wed May 06 16:27:02 2026 +0200
+++ b/LPCSVGHMI/gen_index_xhtml.xslt Tue May 12 14:06:42 2026 +0200
@@ -642,6 +642,8 @@
<xsl:variable name="keypads_descs" select="$parsed_widgets/widget[@type = 'Keypad']"/>
<xsl:variable name="keypads" select="$hmi_elements[@id = $keypads_descs/@id]"/>
+ <xsl:variable name="touchdetect_widget_ids" select="$parsed_widgets/widget[@type = 'TouchDetect']/@id"/> + <xsl:variable name="touchdetect_elements" select="$hmi_elements[@id = $touchdetect_widget_ids]/descendant-or-self::svg:*"/> <func:function name="func:refered_elements">
<xsl:param name="elems"/>
<xsl:variable name="descend" select="$elems/descendant-or-self::svg:*"/>
@@ -692,7 +694,7 @@
<xsl:variable name="required_page_elements" select="func:required_elements($hmi_pages | $keypads)/ancestor-or-self::svg:*"/>
<xsl:variable name="required_list_elements" select="func:refered_elements(($hmi_lists | $hmi_textlists)[@id = $required_page_elements/@id])/ancestor-or-self::svg:*"/>
- <xsl:variable name="required_elements" select="$defs | $required_list_elements | $required_page_elements"/>
+ <xsl:variable name="required_elements" select="$defs | $required_list_elements | $required_page_elements | $touchdetect_elements"/> <xsl:variable name="discardable_elements" select="//svg:*[not(@id = $required_elements/@id)]"/>
<func:function name="func:sumarized_elements">
<xsl:param name="elements"/>
@@ -713,7 +715,7 @@
<xsl:variable name="_detachable_elements" select="func:detachable_elements($hmi_pages | $keypads)"/>
- <xsl:variable name="detachable_elements" select="$_detachable_elements[not(ancestor::*/@id = $_detachable_elements/@id)]"/>
+ <xsl:variable name="detachable_elements" select="$_detachable_elements[not(ancestor::*/@id = $_detachable_elements/@id) and not(@id = $touchdetect_widget_ids)]"/> <declarations:page-class/>
<xsl:template match="declarations:page-class">
@@ -944,7 +946,7 @@
<xsl:for-each select="$in_forEach_widget_ids">
@@ -2605,6 +2607,2240 @@
+ <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label + <xsl:text>HMI:AnimateRotation:optional_args + <xsl:text>Element to rotate is a part of that group labeled "animate". + <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right", + <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element + <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used. + <xsl:text>Optional arguments are: + <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set) + <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set) + <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used) + <xsl:text>The higher the frame rate, the higher CPU usage will be. + <xsl:text>Rotation animation</xsl:text> + <xsl:template match="widget[@type='AnimateRotation']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>AnimateRotationWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> duration = 2000; + <xsl:text> iterations = "infinite"; + <xsl:text> center_x = null; + <xsl:text> center_y = null; + <xsl:text> frame_rate = 10; + <xsl:template match="widget[@type='AnimateRotation']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:variable name="widget_type" select="@type"/> + <xsl:variable name="widget_id" select="@id"/> + <xsl:text> const widget_pos = this.element.getBBox(); + <xsl:text> this.center_x = widget_pos.x + widget_pos.width / 2; + <xsl:text> this.center_y = widget_pos.y + widget_pos.height / 2; + <xsl:for-each select="arg[contains(@value, '=')]"> + <xsl:variable name="name" select="substring-before(@value, '=')"/> + <xsl:variable name="value" select="substring-after(@value, '=')"/> + <xsl:text> this.</xsl:text> + <xsl:value-of select="$name"/> + <xsl:text> = </xsl:text> + <xsl:value-of select="$value"/> + <xsl:variable name="center_element" select="$hmi_element/*[starts-with(@inkscape:label, 'center:')]"/> + <xsl:if test="count($center_element) = 1"> + <xsl:text> var el = id("</xsl:text> + <xsl:value-of select="$center_element/@id"/> + <xsl:variable name="lab" select="substring-after($center_element/@inkscape:label, 'center:')"/> + <xsl:text> var el_label = "</xsl:text> + <xsl:value-of select="$lab"/> + <xsl:text> const el_pos = el.getBBox(); + <xsl:text> switch (el_label) { + <xsl:text> case "top_left": + <xsl:text> this.center_x = el_pos.x; + <xsl:text> this.center_y = el_pos.y; + <xsl:text> case "top_right": + <xsl:text> this.center_x = el_pos.x + el_pos.width; + <xsl:text> this.center_y = el_pos.y; + <xsl:text> case "bottom_left": + <xsl:text> this.center_x = el_pos.x; + <xsl:text> this.center_y = el_pos.y + el_pos.height; + <xsl:text> case "bottom_right": + <xsl:text> this.center_x = el_pos.x + el_pos.width; + <xsl:text> this.center_y = el_pos.y + el_pos.height; + <xsl:text> case "center": + <xsl:text> this.center_x = el_pos.x + el_pos.width / 2; + <xsl:text> this.center_y = el_pos.y + el_pos.height / 2; + <xsl:text> this.element.removeChild(el); + <xsl:if test="count($center_element) > 1"> + <xsl:variable name="errmsg"> + <xsl:value-of select="$widget_type"/> + <xsl:text> widget (id=</xsl:text> + <xsl:value-of select="$widget_id"/> + <xsl:text>) has more than one center element</xsl:text> + <xsl:message terminate="yes"> + <xsl:value-of select="$errmsg"/> + <xsl:variable name="animate_element" select="$hmi_element/*[@inkscape:label = 'animate']"/> + <xsl:if test="count($animate_element) != 1"> + <xsl:variable name="errmsg"> + <xsl:value-of select="$widget_type"/> + <xsl:text> widget (id=</xsl:text> + <xsl:value-of select="$widget_id"/> + <xsl:text>) must have exactly one animate element</xsl:text> + <xsl:message terminate="yes"> + <xsl:value-of select="$errmsg"/> + <xsl:text> var anim_el = id("</xsl:text> + <xsl:value-of select="$animate_element/@id"/> + <xsl:text> anim_el.style.transformOrigin = vsprintf("%.2fpx %.2fpx", [this.center_x, this.center_y]); + <xsl:text> anim_el.style.animation = vsprintf("animateRotation %.3fs steps(%s) %s", [this.duration / 1000.0, this.frame_rate, this.iterations]); + <cssdefs:animaterotation/> + <xsl:template match="cssdefs:animaterotation"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text>@keyframes animateRotation { + <xsl:text> 100% { transform: rotate(360deg); } + <xsl:template match="widget[@type='CloudImage']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by + <xsl:text>link to the file whose name is the value of given variable, being served from + <xsl:text>/media/data/cloud folder. + <xsl:text>Image display</xsl:text> + <xsl:template match="widget[@type='CloudImage']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>CloudImageWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 5; + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> if (index == 0) { + <xsl:text> this.given_url = "cloudfolder?image=" + value; + <xsl:text> this.ready = true; + <xsl:text> this.request_animate(); + <xsl:template match="widget[@type='CloudImage']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> given_url: "", + <xsl:text> ready: false, + <xsl:text> animate: function(){ + <xsl:text> this.element.setAttribute('href', this.given_url); + <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:image[starts-with(@inkscape:label, 'HMI:CloudImage')]"> + <xsl:apply-templates mode="inline_svg" select="@*[not(contains(name(), 'href'))] | node()"/> + <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>DropDownIndexed widget can have one, two or three path variables. + <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text), + <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect) + <xsl:text>labeled elements. + <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the + <xsl:text>limit of available space in page, and "box" is extended to contain all + <xsl:text>"highlight" is moved over pre-selected entry. + <xsl:text>The first variable path is index of selection, and the second is value of selection. + <xsl:text>In case there are one or two path variables, a list of texts is defined via + <xsl:text>If there are no arguments, it is expected that "text" labeled element is of + <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget. + <xsl:text>In that case list of texts is set to TextList content. + <xsl:text>When only one argument is given and its value is "#langs" then list of + <xsl:text>texts is automatically set to the human-readable list of supported + <xsl:text>languages by this HMI. + <xsl:text>Otherwise, arguments are used as dropdown options. + <xsl:text>In case there are three path variables, the third path variable is a filter + <xsl:text>in a form of a string containing ':' separated list of indices of the options + <xsl:text>from the arguments that will be shown in the dropdown. + <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE + <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER + <xsl:text>Let user select text entry in a drop-down menu</xsl:text> + <arg name="entries" count="many" accepts="string"> + <xsl:text>drop-down menu entries</xsl:text> + <path name="selected_index" accepts="HMI_INT"> + <xsl:text>selection index</xsl:text> + <path name="selected_value" accepts="HMI_STRING"> + <xsl:text>selection value</xsl:text> + <path name="filter" accepts="HMI_STRING"> + <xsl:text>indices of shown drop-down menu entries</xsl:text> + <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>DropDownIndexedWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> dispatch(value, old_val, index) { + <xsl:text> if (index == 0) { + <xsl:text> if (!this.opened) this.set_selection(value); + <xsl:text> } else if (index == 2) { + <xsl:text> const desiredIndices = value.split(":").map((str) => +str); + <xsl:text> // Cache the original content to prevent data destruction on subsequent filters + <xsl:text> if (!this.original_content) { + <xsl:text> this.original_content = [...this.content]; + <xsl:text> this.content = this.original_content.filter((item, idx) => desiredIndices.includes(idx)); + <declarations:DropDownIndexed/> + <xsl:template match="declarations:DropDownIndexed"> + <xsl:text>/* </xsl:text> + <xsl:value-of select="local-name()"/> + <xsl:text> Object.getOwnPropertyNames(DropDownWidget.prototype).forEach(name => { + <xsl:text> if (name !== "constructor" && name !== "dispatch") { + <xsl:text> DropDownIndexedWidget.prototype[name] = DropDownWidget.prototype[name]; + <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>box button highlight</xsl:text> + <xsl:variable name="text_elt" select="$hmi_element//*[@inkscape:label='text'][1]"/> + <xsl:text>init_specific: function() { + <xsl:when test="count(arg) = 1 and arg[1]/@value = '#langs'"> + <xsl:text> this.text_elt = id("</xsl:text> + <xsl:value-of select="$text_elt/@id"/> + <xsl:text> this.content = langs.map(([lname,lcode]) => lname); + <xsl:when test="count(arg) = 0"> + <xsl:if test="not($text_elt[self::svg:use])"> + <xsl:message terminate="yes"> + <xsl:text>No argument for HMI:DropDownIndexed widget id="</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>" and "text" labeled element is not a svg:use element</xsl:text> + <xsl:variable name="real_text_elt" select="$result_widgets[@id = $hmi_element/@id]//*[@original=$text_elt/@id]/svg:text"/> + <xsl:text> this.text_elt = id("</xsl:text> + <xsl:value-of select="$real_text_elt/@id"/> + <xsl:variable name="from_list_id" select="substring-after($text_elt/@xlink:href,'#')"/> + <xsl:variable name="from_list" select="$hmi_textlists[(@id | */@id) = $from_list_id]"/> + <xsl:if test="count($from_list) = 0"> + <xsl:message terminate="yes"> + <xsl:text>HMI:DropDownIndexed widget id="</xsl:text> + <xsl:value-of select="$hmi_element/@id"/> + <xsl:text>" "text" labeled element does not point to a svg:text owned by a HMI:List widget</xsl:text> + <xsl:text> this.content = hmi_widgets["</xsl:text> + <xsl:value-of select="$from_list/@id"/> + <xsl:text> this.text_elt = id("</xsl:text> + <xsl:value-of select="$text_elt/@id"/> + <xsl:text> this.content = [ + <xsl:for-each select="arg"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@value"/> + <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis, + <xsl:text>grid/marks, legends and curves. + <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing: + <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels. + <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be + <xsl:text> duplicated along axis line to form intervals marks. + <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their + <xsl:text> bounding box is the chart wall. + <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used + <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths. + <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored. + <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text> + <path name="value" count="1+" accepts="HMI_INT,HMI_REAL"> + <xsl:text>value</xsl:text> + <arg name="xformat" count="optional" accepts="string"> + <xsl:text>format string for X label</xsl:text> + <arg name="yformat" count="optional" accepts="string"> + <xsl:text>format string for Y label</xsl:text> + <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>HistoryXYGraphWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 1; + <xsl:text> this.params = [null, null]; + <xsl:text> [this.x_format, this.y_format] = this.args; + <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this); + <xsl:text> this.loading = false; + <xsl:text> this.curves = []; + <xsl:text> this.curves_data = []; + <xsl:text> this.init_specific(); + <xsl:text> this.reference = new ReferenceFrame( + <xsl:text> [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt], + <xsl:text> [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]], + <xsl:text> [this.x_axis_label_elt, this.y_axis_label_elt], + <xsl:text> [this.x_axis_line_elt, this.y_axis_line_elt], + <xsl:text> [this.x_format, this.y_format]); + <xsl:text> let max_stroke_width = 0; + <xsl:text> for (let curve of this.curves) { + <xsl:text> if (curve.style.strokeWidth > max_stroke_width) { + <xsl:text> max_stroke_width = curve.style.strokeWidth; + <xsl:text> this.curves_data.push([]); + <xsl:text> this.params.push(null); + <xsl:text> this.Margins = this.reference.getLengths().map(length => max_stroke_width / length); + <xsl:text> // create <clipPath> path and attach it to widget + <xsl:text> let clipPath = document.createElementNS(xmlns, "clipPath"); + <xsl:text> let clipPathPath = document.createElementNS(xmlns, "path"); + <xsl:text> let clipPathPathDattr = document.createAttribute("d"); + <xsl:text> clipPathPathDattr.value = this.reference.getClipPathPathDattr(); + <xsl:text> clipPathPath.setAttributeNode(clipPathPathDattr); + <xsl:text> clipPath.appendChild(clipPathPath); + <xsl:text> clipPath.id = randomId(); + <xsl:text> this.element.appendChild(clipPath); + <xsl:text> // assign created clipPath to clip-path property of curves + <xsl:text> for(let curve of this.curves) { + <xsl:text> curve.setAttribute("clip-path", "url(#" + clipPath.id + ")"); + <xsl:text> fetch_error(e){ + <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id); + <xsl:text> do_http_request() { + <xsl:text> this.abort_controller = new AbortController(); + <xsl:text> const decoder = new TextDecoder(); + <xsl:text> let partialChunk = ''; + <xsl:text> const query = { + <xsl:text> startTime: Date.parse(this.params[0]), + <xsl:text> endTime: Date.parse(this.params[1]), + <xsl:text> variableNames: this.params.slice(2) + <xsl:text> const options = { + <xsl:text> method: 'POST', + <xsl:text> body: JSON.stringify(query), + <xsl:text> headers: { 'Content-Type': 'application/json' }, + <xsl:text> signal: this.abort_controller.signal + <xsl:text> return fetch('/history', options) + <xsl:text> .then(response => { + <xsl:text> const reader = response.body.getReader(); + <xsl:text> const read = () => { + <xsl:text> return reader.read().then(({ value, done }) => { + <xsl:text> if (done) return; + <xsl:text> const chunk = decoder.decode(value, { stream: true }); + <xsl:text> const lines = (partialChunk + chunk).split(String.fromCharCode(10)); + <xsl:text> partialChunk = lines.pop(); + <xsl:text> lines.forEach(line => { + <xsl:text> if (line.trim()) { + <xsl:text> const row = JSON.parse(line); + <xsl:text> const vi = query.variableNames.findIndex(v => v === row.varname); + <xsl:text> if (vi !== -1 && this.curves_data[vi]) { + <xsl:text> this.curves_data[vi].push([row.timestamp, row.value]); + <xsl:text> if (row.value > this.ymax) this.ymax = row.value; + <xsl:text> if (row.value < this.ymin) this.ymin = row.value; + <xsl:text> return read(); + <xsl:text> return read(); + <xsl:text> }).catch(this.fetch_error_bound); + <xsl:text> if (this.abort_controller) { + <xsl:text> this.abort_controller.abort(); + <xsl:text> super.unsub(); + <xsl:text> sub(...args){ + <xsl:text> super.sub(...args); + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> this.params[index] = value; + <xsl:text> if (this.params.every((item) => item !== null)) { + <xsl:text> if(!this.loading){ + <xsl:text> this.loading = true; + <xsl:text> this.curves_data = []; + <xsl:text> for (let curve of this.curves) { + <xsl:text> this.curves_data.push([]); + <xsl:text> this.ymin = Infinity; + <xsl:text> this.ymax = -Infinity; + <xsl:text> this.do_http_request().finally(() => { + <xsl:text> let xmin = Infinity; + <xsl:text> let xmax = -Infinity; + <xsl:text> let has_data = false; + <xsl:text> for (let i = 0; i < this.curves.length; i++) { + <xsl:text> const dataLength = this.curves_data[i].length; + <xsl:text> if (dataLength > 1) { + <xsl:text> const ximin = this.curves_data[i][0][0]; + <xsl:text> const ximax = this.curves_data[i][dataLength - 1][0]; + <xsl:text> if (ximin < xmin) xmin = ximin; + <xsl:text> if (ximax > xmax) xmax = ximax; + <xsl:text> has_data = true; + <xsl:text> if (has_data) { + <xsl:text> this.xmin = xmin; + <xsl:text> this.xmax = xmax; + <xsl:text> this.xmin = Date.parse(this.params[0]); + <xsl:text> this.xmax = Date.parse(this.params[1]); + <xsl:text> this.ymin = -1; + <xsl:text> this.ymax = 1; + <xsl:text> let Xrange = this.xmax - this.xmin; + <xsl:text> let Yrange = this.ymax - this.ymin; + <xsl:text> // apply margin by moving min and max to enlarge range + <xsl:text> let [xMargin, yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m, l]) => m * l); + <xsl:text> [[this.dxmin, this.dxmax], [this.dymin, this.dymax]] = + <xsl:text> [[this.xmin - xMargin, this.xmax + xMargin], + <xsl:text> [this.ymin - yMargin, this.ymax + yMargin]]; + <xsl:text> Xrange += 2 * xMargin; + <xsl:text> Yrange += 2 * yMargin; + <xsl:text> // recompute curves "d" attribute + <xsl:text> let [base_point, xvect, yvect] = this.reference.getBaseRef(); + <xsl:text> this.curves_d_attr = + <xsl:text> zip(this.curves_data, this.curves).map(([data, curve]) => { + <xsl:text> let new_d = data.map(([x, y], i) => { + <xsl:text> // compute curve point from data, ranges, and base_ref + <xsl:text> let xv = vectorscale(xvect, (x - this.dxmin) / Xrange); + <xsl:text> let yv = vectorscale(yvect, (y - this.dymin) / Yrange); + <xsl:text> let px = base_point.x + xv.x + yv.x; + <xsl:text> let py = base_point.y + xv.y + yv.y; + <xsl:text> return " " + px + "," + py; + <xsl:text> new_d.unshift("M "); + <xsl:text> return new_d.join(''); + <xsl:text> // computed curves "d" attr is applied to svg curve during animate(); + <xsl:text> this.request_animate(); + <xsl:text> this.loading = false; + <xsl:text> this.reference.applyRanges([[this.dxmin, this.dxmax], + <xsl:text> [this.dymin, this.dymax]]); + <xsl:text> // apply computed curves "d" attributes + <xsl:text> for (let [curve, d_attr] of zip(this.curves, this.curves_d_attr)) { + <xsl:text> if (d_attr.length > 2) + <xsl:text> curve.setAttribute("d", d_attr); + <xsl:text> curve.setAttribute("d", "M 0 0"); + <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label</xsl:text> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label</xsl:text> + <xsl:text> init_specific() { + <xsl:variable name="curves" select="$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]"/> + <xsl:variable name="curves_error" select="func:check_curves_label_consistency($curves,count($curves)-1)"/> + <xsl:if test="string-length($curves_error)"> + <xsl:message terminate="yes"> + <xsl:text>HistoryXYGraph id="</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>", label="</xsl:text> + <xsl:value-of select="@inkscape:label"/> + <xsl:text>" : </xsl:text> + <xsl:value-of select="$curves_error"/> + <xsl:for-each select="$curves"> + <xsl:variable name="label" select="@inkscape:label"/> + <xsl:variable name="_id" select="@id"/> + <xsl:variable name="curve_num" select="substring(@inkscape:label, 7)"/> + <xsl:text> this.curves[</xsl:text> + <xsl:value-of select="$curve_num"/> + <xsl:text>] = id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>"); /* </xsl:text> + <xsl:value-of select="@inkscape:label"/> + <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in + <xsl:text>SVG sub-elements of "data" labeled element. + <xsl:text>Documentation to be written. see svghmi example. + <xsl:text>Http POST variables, spread JSON back</xsl:text> + <arg name="url" accepts="string"/> + <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING"> + <xsl:text>single variable to edit</xsl:text> + <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>MultiLangJsonTableWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> // arbitrary defaults to avoid missing entries in query + <xsl:text> cache = [0,0,0]; + <xsl:text> init_common() { + <xsl:text> this.spread_json_data_bound = this.spread_json_data.bind(this); + <xsl:text> this.handle_http_response_bound = this.handle_http_response.bind(this); + <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this); + <xsl:text> if (this.should_translate === undefined) { + <xsl:text> this.should_translate = []; + <xsl:text> if (this.lang_keys === undefined) { + <xsl:text> this.lang_keys = []; + <xsl:text> this.promised = false; + <xsl:text> handle_http_response(response) { + <xsl:text> if (!response.ok) { + <xsl:text> console.log("HTTP error, status = " + response.status); + <xsl:text> return response.json(); + <xsl:text> fetch_error(e){ + <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id); + <xsl:text> do_http_request(...opt) { + <xsl:text> this.abort_controller = new AbortController(); + <xsl:text> return Promise.resolve().then(() => { + <xsl:text> const query = { + <xsl:text> args: this.args, + <xsl:text> range: this.cache[1], + <xsl:text> position: this.cache[2], + <xsl:text> visible: this.visible, + <xsl:text> extra: this.cache.slice(4), + <xsl:text> options: opt + <xsl:text> const options = { + <xsl:text> method: 'POST', + <xsl:text> body: JSON.stringify(query), + <xsl:text> headers: {'Content-Type': 'application/json'}, + <xsl:text> signal: this.abort_controller.signal + <xsl:text> return fetch(this.args[0], options) + <xsl:text> .then(this.handle_http_response_bound) + <xsl:text> .then(this.spread_json_data_bound) + <xsl:text> .catch(this.fetch_error_bound); + <xsl:text> this.abort_controller.abort(); + <xsl:text> super.unsub(); + <xsl:text> sub(...args){ + <xsl:text> this.cache[0] = undefined; + <xsl:text> super.sub(...args); + <xsl:text> dispatch(value, oldval, index) { + <xsl:text> if(this.cache[index] != value) + <xsl:text> this.cache[index] = value; + <xsl:text> if(!this.promised){ + <xsl:text> this.promised = true; + <xsl:text> this.do_http_request().finally(() => { + <xsl:text> this.promised = false; + <xsl:text> make_on_click(...options){ + <xsl:text> let that = this; + <xsl:text> return function(evt){ + <xsl:text> that.do_http_request(...options); + <xsl:text> // on_click(evt, ...options) { + <xsl:text> // this.do_http_request(...options); + <xsl:template mode="json_table_elt_render" match="svg:*"> + <xsl:message terminate="yes"> + <xsl:text>MultiLangJsonTable Widget can't contain element of type </xsl:text> + <xsl:value-of select="local-name()"/> + <func:function name="func:ml_json_expressions"> + <xsl:param name="expressions"/> + <xsl:param name="label"/> + <xsl:when test="$label"> + <xsl:variable name="suffixes" select="str:split($label)"/> + <xsl:variable name="res"> + <xsl:for-each select="$suffixes"> + <xsl:variable name="suffix" select="."/> + <xsl:variable name="pos" select="position()"/> + <xsl:variable name="expr" select="$expressions[position() <= $pos][last()]/expression"/> + <xsl:if test="$pos = 1"> + <xsl:variable name="raw_selector" select="$suffix"/> + <xsl:variable name="lang_selector"> + <xsl:when test="starts-with($raw_selector, '.')"> + <xsl:value-of select="substring($raw_selector, 2)"/> + <xsl:value-of select="$raw_selector"/> + <xsl:attribute name="lang_selector"> + <xsl:value-of select="$lang_selector"/> + <xsl:when test="contains($suffix, '=')"> + <xsl:variable name="name" select="substring-before($suffix, '=')"/> + <xsl:variable name="content_raw" select="substring-after($suffix, '=')"/> + <xsl:if test="$expr/@name[. != $name]"> + <xsl:message terminate="yes"> + <xsl:text>MultiLangJsonTable : misplaced '=' or inconsistent names in Json data expressions.</xsl:text> + <xsl:attribute name="name"> + <xsl:value-of select="$name"/> + <xsl:when test="starts-with($content_raw, '_(') and substring($content_raw, string-length($content_raw)) = ')'"> + <xsl:variable name="raw_key" select="substring($content_raw, 3, string-length($content_raw) - 3)"/> + <xsl:variable name="clean_key"> + <xsl:when test="starts-with($raw_key, '.')"> + <xsl:value-of select="substring($raw_key, 2)"/> + <xsl:value-of select="$raw_key"/> + <xsl:attribute name="translation_key"> + <xsl:value-of select="$clean_key"/> + <xsl:attribute name="content"> + <xsl:value-of select="$expr/@content"/> + <xsl:value-of select="$raw_key"/> + <xsl:attribute name="content"> + <xsl:value-of select="$expr/@content"/> + <xsl:value-of select="$content_raw"/> + <xsl:copy-of select="$expr/@name"/> + <xsl:attribute name="content"> + <xsl:value-of select="$expr/@content"/> + <xsl:value-of select="$suffix"/> + <func:result select="exsl:node-set($res)"/> + <func:result select="$expressions"/> + <xsl:variable name="ml_initexpr"> + <xsl:attribute name="content"> + <xsl:text>jdata</xsl:text> + <xsl:variable name="ml_initexpr_ns" select="exsl:node-set($ml_initexpr)"/> + <xsl:template mode="json_table_elt_render" match="svg:use"> + <xsl:param name="expressions"/> + <xsl:variable name="targetid" select="substring-after(@xlink:href,'#')"/> + <xsl:variable name="from_list" select="$hmi_lists[(@id | */@id) = $targetid]"/> + <xsl:when test="count($from_list) > 0"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").href.baseVal = + <xsl:text> "#"+hmi_widgets["</xsl:text> + <xsl:value-of select="$from_list/@id"/> + <xsl:text>"].items[</xsl:text> + <xsl:value-of select="$expressions/expression[1]/@content"/> + <xsl:message terminate="no"> + <xsl:text>Clones (svg:use) in MultiLangJsonTable Widget must point to a valid HMI:List widget or item. Reference "</xsl:text> + <xsl:value-of select="@xlink:href"/> + <xsl:text>" is not valid and will not be updated.</xsl:text> + <xsl:template mode="json_table_elt_render" match="svg:text"> + <xsl:param name="expressions"/> + <xsl:variable name="value_expr" select="$expressions/expression[1]/@content"/> + <xsl:variable name="original" select="@original"/> + <xsl:variable name="from_textstylelist" select="$textstylelist_related_ns/list[elt/@eltid = $original]"/> + <xsl:when test="count($from_textstylelist) > 0"> + <xsl:variable name="content_expr" select="$expressions/expression[2]/@content"/> + <xsl:if test="string-length($content_expr) = 0 or $expressions/expression[2]/@name != 'textContent'"> + <xsl:message terminate="yes"> + <xsl:text>Clones (svg:use) in MultiLangJsonTable Widget pointing to a HMI:TextStyleList widget or item must have a "textContent=.someVal" assignment following value expression in label.</xsl:text> + <xsl:text> let elt = id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text> elt.textContent = String(</xsl:text> + <xsl:value-of select="$content_expr"/> + <xsl:text> elt.style = hmi_widgets["</xsl:text> + <xsl:value-of select="$from_textstylelist/@listid"/> + <xsl:text>"].styles[</xsl:text> + <xsl:value-of select="$value_expr"/> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").textContent = String(</xsl:text> + <xsl:value-of select="$value_expr"/> + <xsl:template mode="json_table_elt_render" match="svg:image"> + <xsl:param name="expressions"/> + <xsl:variable name="value_expr" select="$expressions/expression[1]/@content"/> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").setAttribute('href', String(</xsl:text> + <xsl:value-of select="$value_expr"/> + <xsl:template mode="json_table_render_except_comments" match="svg:*"> + <xsl:param name="expressions"/> + <xsl:param name="widget_elts"/> + <xsl:variable name="label" select="func:filter_non_widget_label(., $widget_elts)"/> + <xsl:if test="not(starts-with($label,'#'))"> + <xsl:apply-templates mode="json_table_render" select="."> + <xsl:with-param name="expressions" select="$expressions"/> + <xsl:with-param name="widget_elts" select="$widget_elts"/> + <xsl:with-param name="label" select="$label"/> + <xsl:template mode="json_table_render" match="svg:*"> + <xsl:param name="expressions"/> + <xsl:param name="widget_elts"/> + <xsl:param name="label"/> + <xsl:variable name="new_expressions" select="func:ml_json_expressions($expressions, $label)"/> + <xsl:variable name="elt" select="."/> + <xsl:for-each select="$new_expressions/expression[position() > 1][starts-with(@name,'onClick')]"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="$elt/@id"/> + <xsl:text>").onclick = this.make_on_click('</xsl:text> + <xsl:value-of select="@name"/> + <xsl:text>', </xsl:text> + <xsl:value-of select="@content"/> + <xsl:apply-templates mode="json_table_elt_render" select="."> + <xsl:with-param name="expressions" select="$new_expressions"/> + <xsl:template mode="json_table_render" match="svg:g"> + <xsl:param name="expressions"/> + <xsl:param name="widget_elts"/> + <xsl:param name="label"/> + <xsl:variable name="varprefix"> + <xsl:text>obj_</xsl:text> + <xsl:value-of select="@id"/> + <xsl:for-each select="$expressions/expression"> + <xsl:text> let </xsl:text> + <xsl:value-of select="$varprefix"/> + <xsl:value-of select="position()"/> + <xsl:text> = </xsl:text> + <xsl:value-of select="@content"/> + <xsl:text> if(</xsl:text> + <xsl:value-of select="$varprefix"/> + <xsl:value-of select="position()"/> + <xsl:text> == undefined) { + <xsl:variable name="new_expressions"> + <xsl:for-each select="$expressions/expression"> + <xsl:copy-of select="@name"/> + <xsl:attribute name="content"> + <xsl:value-of select="$varprefix"/> + <xsl:value-of select="position()"/> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").style = "</xsl:text> + <xsl:value-of select="@style"/> + <xsl:apply-templates mode="json_table_render_except_comments" select="*"> + <xsl:with-param name="expressions" select="func:ml_json_expressions(exsl:node-set($new_expressions), $label)"/> + <xsl:with-param name="widget_elts" select="$widget_elts"/> + <xsl:text> } catch(err) { + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").style = "display:none"; + <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>data</xsl:text> + <xsl:variable name="data_elt" select="$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']"/> + <xsl:variable name="widget_elts" select="$hmi_element/*[@inkscape:label = 'data']/descendant::svg:*"/> + <xsl:variable name="all_parsed_expressions"> + <xsl:for-each select="$data_elt/descendant-or-self::svg:*[@inkscape:label]"> + <xsl:variable name="exprs" select="func:ml_json_expressions($ml_initexpr_ns, @inkscape:label)"/> + <xsl:copy-of select="exsl:node-set($exprs)//expression[@translation_key or @lang_selector]"/> + <xsl:variable name="all_exprs_ns" select="exsl:node-set($all_parsed_expressions)"/> + <xsl:text> visible: </xsl:text> + <xsl:value-of select="count($data_elt/*[@inkscape:label])"/> + <xsl:text> should_translate: [ + <xsl:for-each select="$all_exprs_ns/expression[@translation_key]"> + <xsl:if test="not(@translation_key = preceding-sibling::expression/@translation_key)"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@translation_key"/> + <xsl:if test="position() != last()"> + <xsl:text> lang_keys: [ + <xsl:for-each select="$all_exprs_ns/expression[@lang_selector]"> + <xsl:if test="not(@lang_selector = preceding-sibling::expression/@lang_selector)"> + <xsl:text> "</xsl:text> + <xsl:value-of select="@lang_selector"/> + <xsl:if test="position() != last()"> + <xsl:text> spread_json_data: function(janswer) { + <xsl:text> let [range,position,jdata] = janswer; + <xsl:text> if (jdata.length > 0 && this.should_translate.length > 0) { + <xsl:text> const lang = cache[lang_local_index]; + <xsl:text> const langcode = langs[lang][1]; + <xsl:text> for (let row of jdata) { + <xsl:text> for (const key of this.should_translate) { + <xsl:text> if (key in row) { + <xsl:text> const orig = row[key]; + <xsl:text> const match = translations.find(item => item[1][0] == orig); + <xsl:text> const tr = match ? match[1][lang] : orig; + <xsl:text> row[key] = tr; + <xsl:text> for (const key of this.lang_keys) { + <xsl:text> if (key in row) { + <xsl:text> row[key] = String(row[key]) + "_" + langcode; + <xsl:text> [[1, range], [2, position], [3, this.visible]].map(([i,v]) => { + <xsl:text> this.apply_hmi_value(i,v); + <xsl:text> this.cache[i] = v; + <xsl:apply-templates mode="json_table_render_except_comments" select="$data_elt"> + <xsl:with-param name="expressions" select="$ml_initexpr_ns"/> + <xsl:with-param name="widget_elts" select="$widget_elts"/> + <xsl:text> this.init_common(); + <xsl:for-each select="$hmi_element/*[starts-with(@inkscape:label,'action_')]"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").onclick = this.make_on_click("</xsl:text> + <xsl:value-of select="func:escape_quotes(@inkscape:label)"/> + <xsl:for-each select="$hmi_element/*[starts-with(@inkscape:label,'dict')]"> + <xsl:text> id("</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>").style.display = "none"; + <xsl:template match="widget[@type='Swipe']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes + <xsl:text>associated actions. The widget should be placed on top of the area where the + <xsl:text>movement should be detected. It is a group containing a graphical element + <xsl:text>"area" which defines the area where the swipe should be detected. + <xsl:text>For each of the motions to be detected there must exist several parameters + <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set: + <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents + <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of + <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to + <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set + <xsl:text>to). change and set commands should also be accompanied by paths with the + <xsl:text>same name and their values should be variable names to apply the command to. + <xsl:text>Additional parameters to add are: + <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer + <xsl:text> movement. Anything below that value will not be considered + <xsl:text> a movement. If omitted, 5 will be used. + <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer + <xsl:text> down event. If time elapses without any significant movement + <xsl:text> (defined by movethreshold), the pointer down/click event + <xsl:text> will be propagated on an element on a lower level than the + <xsl:text> swiping area. Similar thing will happen on pointer up event + <xsl:text> if there was no significant movement. + <xsl:text>HMI:Swipe:movethreshold=3:left_xthreshold=30:left_ythreshold=5:left_jump=Home:up_xthreshold=25:up_ythreshold=5:up_change=+2@up_change=/VAR0 + <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must + <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget + <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect + <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and + <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2. + <xsl:text>Detect swipe motion and react accordingly</xsl:text> + <xsl:template match="widget[@type='Swipe']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>SwipeWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> frequency = 2; + <xsl:text> startX = -1; + <xsl:text> startY = -1; + <xsl:text> moveThreshold = 5; + <xsl:text> pressTimeout = 300; + <xsl:text> touchTimer = null; + <xsl:text> settings = { + <xsl:text> actions: [], + <xsl:text> xThreshold: 100, + <xsl:text> yThreshold: 0, + <xsl:text> actions: [], + <xsl:text> xThreshold: 100, + <xsl:text> yThreshold: 0, + <xsl:text> actions: [], + <xsl:text> xThreshold: 0, + <xsl:text> yThreshold: 100, + <xsl:text> actions: [], + <xsl:text> xThreshold: 0, + <xsl:text> yThreshold: 100, + <xsl:text> propagateMouseDownEvent(simulateUp) { + <xsl:text> const elements = document.elementsFromPoint(this.startX, this.startY); + <xsl:text> if (elements.length > 1) { + <xsl:text> const eventDown = new MouseEvent("pointerdown", { + <xsl:text> view: window, + <xsl:text> bubbles: true, + <xsl:text> cancelable: true, + <xsl:text> const eventClick = new MouseEvent("click", { + <xsl:text> view: window, + <xsl:text> bubbles: true, + <xsl:text> cancelable: true, + <xsl:text> const eventUp = new MouseEvent("pointerup", { + <xsl:text> view: window, + <xsl:text> bubbles: true, + <xsl:text> cancelable: true, + <xsl:text> const cb = document.getElementById(elements[1].id); + <xsl:text> cb.dispatchEvent(eventDown); + <xsl:text> cb.dispatchEvent(eventClick); + <xsl:text> if (simulateUp) { + <xsl:text> window.setTimeout(() => { + <xsl:text> cb.dispatchEvent(eventUp); + <xsl:text> onMouseUp(evt) { + <xsl:text> window.clearTimeout(this.touchTimer); + <xsl:text> this.touchTimer = null; + <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true); + <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true); + <xsl:text> const area = this.element.getBoundingClientRect(); + <xsl:text> var xDiff = (evt.pageX - this.startX) * 100.0 / area.width; + <xsl:text> var yDiff = (evt.pageY - this.startY) * 100.0 / area.height; + <xsl:text> var action = null; + <xsl:text> if (xDiff < 0 && Math.abs(xDiff) >= this.settings.left.xThreshold && Math.abs(yDiff) < this.settings.left.yThreshold) { + <xsl:text> action = "left"; + <xsl:text> } else if (xDiff > 0 && Math.abs(xDiff) >= this.settings.right.xThreshold && Math.abs(yDiff) < this.settings.right.yThreshold) { + <xsl:text> action = "right"; + <xsl:text> } else if (yDiff < 0 && Math.abs(yDiff) >= this.settings.up.yThreshold && Math.abs(xDiff) < this.settings.up.xThreshold) { + <xsl:text> action = "up"; + <xsl:text> } else if (yDiff > 0 && Math.abs(yDiff) >= this.settings.down.yThreshold && Math.abs(xDiff) < this.settings.down.xThreshold) { + <xsl:text> action = "down"; + <xsl:text> } else if (Math.abs(xDiff) < this.moveThreshold && Math.abs(yDiff) < this.moveThreshold) { + <xsl:text> this.propagateMouseDownEvent(true); + <xsl:text> if (action) { + <xsl:text> for (var a of this.settings[action].actions) { + <xsl:text> if (a.action == "jump") { + <xsl:text> fading_page_switch(a.target); + <xsl:text> } else if (a.action == "change") { + <xsl:text> this.change_hmi_value(a.var_idx, a.value); + <xsl:text> } else if (a.action == "set") { + <xsl:text> this.apply_hmi_value(a.var_idx, a.value); + <xsl:text> onMouseMove(evt) { + <xsl:text> this.currX = evt.pageX; + <xsl:text> this.currY = evt.pageY; + <xsl:text> onMouseDown(evt) { + <xsl:text> this.startX = evt.pageX; + <xsl:text> this.startY = evt.pageY; + <xsl:text> this.currX = evt.pageX; + <xsl:text> this.currY = evt.pageY; + <xsl:text> svg_root.addEventListener("pointerup", this.boundOnMouseUp, true); + <xsl:text> svg_root.addEventListener("pointermove", this.boundOnMouseMove, true); + <xsl:text> this.touchTimer = window.setTimeout(() => { + <xsl:text> const area = this.element.getBBox(); + <xsl:text> var xDiff = (this.currX - this.startX) * 100.0 / area.width; + <xsl:text> var yDiff = (this.currY - this.startY) * 100.0 / area.height; + <xsl:text> if (Math.abs(xDiff) < this.moveThreshold && Math.abs(yDiff) < this.moveThreshold) { + <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true); + <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true); + <xsl:text> this.propagateMouseDownEvent(false); + <xsl:text> this.touchTimer = null; + <xsl:text> }, this.pressTimeout); + <xsl:text> this.request_animate(); + <xsl:template match="widget[@type='Swipe']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:text> init: function() { + <xsl:text> this.boundOnMouseUp = this.onMouseUp.bind(this); + <xsl:text> this.boundOnMouseMove = this.onMouseMove.bind(this); + <xsl:text> this.element.addEventListener("pointerdown", this.onMouseDown.bind(this)); + <xsl:text> const dirs = ["left", "right", "up", "down"]; + <xsl:text> var properDir = false; + <xsl:text> var pathIndex = -1; + <xsl:variable name="paths" select="path"/> + <xsl:for-each select="arg[contains(@value, '=')]"> + <xsl:variable name="name" select="substring-before(@value, '=')"/> + <xsl:variable name="value" select="substring-after(@value, '=')"/> + <xsl:variable name="index"> + <xsl:for-each select="$paths"> + <xsl:if test="@assign = $name"> + <xsl:value-of select="position()-1"/> + <xsl:variable name="direction" select="substring-before($name, '_')"/> + <xsl:variable name="command" select="substring-after($name, '_')"/> + <xsl:text> if ("</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text>".length > 0) { + <xsl:text> pathIndex = Number("</xsl:text> + <xsl:value-of select="$index"/> + <xsl:text> properDir = dirs.findIndex((x) => x == "</xsl:text> + <xsl:value-of select="$direction"/> + <xsl:text> if (properDir) { + <xsl:text> switch ("</xsl:text> + <xsl:value-of select="$command"/> + <xsl:text> case "xthreshold": + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$direction"/> + <xsl:text>"].xThreshold = </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> case "ythreshold": + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$direction"/> + <xsl:text>"].yThreshold = </xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> case "jump": + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$direction"/> + <xsl:text>"].actions.push({ + <xsl:text> action: "jump", + <xsl:text> target: "</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> case "change": + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$direction"/> + <xsl:text>"].actions.push({ + <xsl:text> action: "change", + <xsl:text> var_idx: pathIndex, + <xsl:text> value: "</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> this.settings["</xsl:text> + <xsl:value-of select="$direction"/> + <xsl:text>"].actions.push({ + <xsl:text> action: "set", + <xsl:text> var_idx: pathIndex, + <xsl:text> value: "</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> if ("</xsl:text> + <xsl:value-of select="$name"/> + <xsl:text>" == "movethreshold") { + <xsl:text> this.moveThreshold = "</xsl:text> + <xsl:value-of select="$value"/> + <xsl:text> } else if ("</xsl:text> + <xsl:value-of select="$name"/> + <xsl:text>" == "presstimeout") { + <xsl:text> this.pressTimeout = "</xsl:text> + <xsl:value-of select="$value"/> + <xsl:template match="widget[@type='TouchDetect']" mode="widget_desc"> + <xsl:value-of select="@type"/> + <xsl:text>TouchDetect publishes whether any finger is on the screen via one HMI variable (first path). + <xsl:text>Place the group under the document svg as a sibling of page groups (it is not tied to any page's + <xsl:text>widget list). init() sets relativeness/offset so get_variable_index works without calling sub() + <xsl:text>(sub() registers with subscribers and runs apply_cache; not needed for outbound-only touch writes). + <xsl:text>Label example: HMI:TouchDetect@/YourBoolOrInt — while at least one touch pointer is active the + <xsl:text>variable is 1, otherwise 0. Listeners are on document.body (pointerdown/pointerup/pointercancel). + <xsl:text>Global touch active flag (body pointer events, touch pointers only)</xsl:text> + <path name="touch_active" count="1" accepts="HMI_BOOL,HMI_INT"/> + <xsl:template match="widget[@type='TouchDetect']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>TouchDetectWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> const n = this.indexes ? this.indexes.length : 0; + <xsl:text> if (n > 0) { + <xsl:text> this.offset = 0; + <xsl:text> this.relativeness = Array(n).fill(false); + <xsl:text> this.container_id = ""; + <xsl:text> this._touchPointerIds = new Set(); + <xsl:text> this._onPointerDown = (e) => { + <xsl:text> if (this._touchPointerIds.size === 0) { + <xsl:text> this.apply_hmi_value(0, 1); + <xsl:text> this._touchPointerIds.add(e.pointerId); + <xsl:text> this._onPointerUp = (e) => { + <xsl:text> this._touchPointerIds.delete(e.pointerId); + <xsl:text> if (this._touchPointerIds.size === 0) { + <xsl:text> this.apply_hmi_value(0, 0); + <xsl:text> document.body.addEventListener("pointerdown", this._onPointerDown, { passive: true }); + <xsl:text> document.body.addEventListener("pointerup", this._onPointerUp, { passive: true }); + <xsl:text> document.body.addEventListener("pointercancel", this._onPointerUp, { passive: true }); + <xsl:template match="widget[@type='TouchDetect']" mode="widget_defs"> + <xsl:param name="hmi_element"/> + <xsl:variable name="disability"> + <xsl:call-template name="defs_by_labels"> + <xsl:with-param name="hmi_element" select="$hmi_element"/> + <xsl:with-param name="labels"> + <xsl:text>/disabled</xsl:text> + <xsl:with-param name="mandatory" select="'no'"/> + <xsl:value-of select="$disability"/> + <xsl:variable name="has_disability" select="string-length($disability)>0"/> + <xsl:if test="count(path) != 1"> + <xsl:message terminate="yes"> + <xsl:text>TouchDetect id="</xsl:text> + <xsl:value-of select="@id"/> + <xsl:text>": exactly one HMI path is required (e.g. HMI:TouchDetect@/PLC_TOUCH_ACTIVE)</xsl:text> + <xsl:template match="widget[@type='VarSync']" mode="widget_class"> + <xsl:text>class </xsl:text> + <xsl:text>VarSyncWidget</xsl:text> + <xsl:text> extends Widget{ + <xsl:text> dispatch(value, oldval, varnum) { + <xsl:text> if (varnum === 0) { + <xsl:text> let dest_index = this.get_variable_index(1); + <xsl:text> let current_dest_val = cache[dest_index]; + <xsl:text> if (value !== current_dest_val) { + <xsl:text> this.apply_hmi_value(1, value); + <xsl:text> else if (varnum === 1) { + <xsl:text> let src_index = this.get_variable_index(0); + <xsl:text> if (value !== cache[src_index]) { + <xsl:text> this.apply_hmi_value(0, value); + <xsl:text> this.element.style.display = "none"; <xsl:template match="widget[@type='Assign']" mode="widget_desc">
<xsl:value-of select="@type"/>
@@ -9380,2140 +11616,6 @@
- <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label
- <xsl:text>HMI:AnimateRotation:optional_args
- <xsl:text>Element to rotate is a part of that group labeled "animate".
- <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right",
- <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element
- <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used.
- <xsl:text>Optional arguments are:
- <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set)
- <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set)
- <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used)
- <xsl:text>The higher the frame rate, the higher CPU usage will be.
- <xsl:text>Rotation animation</xsl:text>
- <xsl:template match="widget[@type='AnimateRotation']" mode="widget_class">
- <xsl:text>class </xsl:text>
- <xsl:text>AnimateRotationWidget</xsl:text>
- <xsl:text> extends Widget{
- <xsl:text> duration = 2000;
- <xsl:text> iterations = "infinite";
- <xsl:text> center_x = null;
- <xsl:text> center_y = null;
- <xsl:text> frame_rate = 10;
- <xsl:template match="widget[@type='AnimateRotation']" mode="widget_defs">
- <xsl:param name="hmi_element"/>
- <xsl:variable name="disability">
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/disabled</xsl:text>
- <xsl:with-param name="mandatory" select="'no'"/>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)>0"/>
- <xsl:variable name="widget_type" select="@type"/>
- <xsl:variable name="widget_id" select="@id"/>
- <xsl:text> const widget_pos = this.element.getBBox();
- <xsl:text> this.center_x = widget_pos.x + widget_pos.width / 2;
- <xsl:text> this.center_y = widget_pos.y + widget_pos.height / 2;
- <xsl:for-each select="arg[contains(@value, '=')]">
- <xsl:variable name="name" select="substring-before(@value, '=')"/>
- <xsl:variable name="value" select="substring-after(@value, '=')"/>
- <xsl:text> this.</xsl:text>
- <xsl:value-of select="$name"/>
- <xsl:text> = </xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:variable name="center_element" select="$hmi_element/*[starts-with(@inkscape:label, 'center:')]"/>
- <xsl:if test="count($center_element) = 1">
- <xsl:text> var el = id("</xsl:text>
- <xsl:value-of select="$center_element/@id"/>
- <xsl:variable name="lab" select="substring-after($center_element/@inkscape:label, 'center:')"/>
- <xsl:text> var el_label = "</xsl:text>
- <xsl:value-of select="$lab"/>
- <xsl:text> const el_pos = el.getBBox();
- <xsl:text> switch (el_label) {
- <xsl:text> case "top_left":
- <xsl:text> this.center_x = el_pos.x;
- <xsl:text> this.center_y = el_pos.y;
- <xsl:text> case "top_right":
- <xsl:text> this.center_x = el_pos.x + el_pos.width;
- <xsl:text> this.center_y = el_pos.y;
- <xsl:text> case "bottom_left":
- <xsl:text> this.center_x = el_pos.x;
- <xsl:text> this.center_y = el_pos.y + el_pos.height;
- <xsl:text> case "bottom_right":
- <xsl:text> this.center_x = el_pos.x + el_pos.width;
- <xsl:text> this.center_y = el_pos.y + el_pos.height;
- <xsl:text> case "center":
- <xsl:text> this.center_x = el_pos.x + el_pos.width / 2;
- <xsl:text> this.center_y = el_pos.y + el_pos.height / 2;
- <xsl:text> this.element.removeChild(el);
- <xsl:if test="count($center_element) > 1">
- <xsl:variable name="errmsg">
- <xsl:value-of select="$widget_type"/>
- <xsl:text> widget (id=</xsl:text>
- <xsl:value-of select="$widget_id"/>
- <xsl:text>) has more than one center element</xsl:text>
- <xsl:message terminate="yes">
- <xsl:value-of select="$errmsg"/>
- <xsl:variable name="animate_element" select="$hmi_element/*[@inkscape:label = 'animate']"/>
- <xsl:if test="count($animate_element) != 1">
- <xsl:variable name="errmsg">
- <xsl:value-of select="$widget_type"/>
- <xsl:text> widget (id=</xsl:text>
- <xsl:value-of select="$widget_id"/>
- <xsl:text>) must have exactly one animate element</xsl:text>
- <xsl:message terminate="yes">
- <xsl:value-of select="$errmsg"/>
- <xsl:text> var anim_el = id("</xsl:text>
- <xsl:value-of select="$animate_element/@id"/>
- <xsl:text> anim_el.style.transformOrigin = vsprintf("%.2fpx %.2fpx", [this.center_x, this.center_y]);
- <xsl:text> anim_el.style.animation = vsprintf("animateRotation %.3fs steps(%s) %s", [this.duration / 1000.0, this.frame_rate, this.iterations]);
- <cssdefs:animaterotation/>
- <xsl:template match="cssdefs:animaterotation">
- <xsl:text>/* </xsl:text>
- <xsl:value-of select="local-name()"/>
- <xsl:text>@keyframes animateRotation {
- <xsl:text> 100% { transform: rotate(360deg); }
- <xsl:template match="widget[@type='CloudImage']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by
- <xsl:text>link to the file whose name is the value of given variable, being served from
- <xsl:text>/media/data/cloud folder.
- <xsl:text>Image display</xsl:text>
- <xsl:template match="widget[@type='CloudImage']" mode="widget_class">
- <xsl:text>class </xsl:text>
- <xsl:text>CloudImageWidget</xsl:text>
- <xsl:text> extends Widget{
- <xsl:text> frequency = 5;
- <xsl:text> dispatch(value, oldval, index) {
- <xsl:text> if (index == 0) {
- <xsl:text> this.given_url = "cloudfolder?image=" + value;
- <xsl:text> this.ready = true;
- <xsl:text> this.request_animate();
- <xsl:template match="widget[@type='CloudImage']" mode="widget_defs">
- <xsl:param name="hmi_element"/>
- <xsl:variable name="disability">
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/disabled</xsl:text>
- <xsl:with-param name="mandatory" select="'no'"/>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)>0"/>
- <xsl:text> given_url: "",
- <xsl:text> ready: false,
- <xsl:text> animate: function(){
- <xsl:text> this.element.setAttribute('href', this.given_url);
- <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:image[starts-with(@inkscape:label, 'HMI:CloudImage')]">
- <xsl:apply-templates mode="inline_svg" select="@*[not(contains(name(), 'href'))] | node()"/>
- <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>DropDownIndexed widget can have one, two or three path variables.
- <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text),
- <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect)
- <xsl:text>labeled elements.
- <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the
- <xsl:text>limit of available space in page, and "box" is extended to contain all
- <xsl:text>"highlight" is moved over pre-selected entry.
- <xsl:text>The first variable path is index of selection, and the second is value of selection.
- <xsl:text>In case there are one or two path variables, a list of texts is defined via
- <xsl:text>If there are no arguments, it is expected that "text" labeled element is of
- <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget.
- <xsl:text>In that case list of texts is set to TextList content.
- <xsl:text>When only one argument is given and its value is "#langs" then list of
- <xsl:text>texts is automatically set to the human-readable list of supported
- <xsl:text>languages by this HMI.
- <xsl:text>Otherwise, arguments are used as dropdown options.
- <xsl:text>In case there are three path variables, the third path variable is a filter
- <xsl:text>in a form of a string containing ':' separated list of indices of the options
- <xsl:text>from the arguments that will be shown in the dropdown.
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER
- <xsl:text>Let user select text entry in a drop-down menu</xsl:text>
- <arg name="entries" count="many" accepts="string">
- <xsl:text>drop-down menu entries</xsl:text>
- <path name="selected_index" accepts="HMI_INT">
- <xsl:text>selection index</xsl:text>
- <path name="selected_value" accepts="HMI_STRING">
- <xsl:text>selection value</xsl:text>
- <path name="filter" accepts="HMI_STRING">
- <xsl:text>indices of shown drop-down menu entries</xsl:text>
- <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_class">
- <xsl:text>class </xsl:text>
- <xsl:text>DropDownIndexedWidget</xsl:text>
- <xsl:text> extends Widget{
- <xsl:text> dispatch(value, old_val, index) {
- <xsl:text> if (index == 0) {
- <xsl:text> if (!this.opened) this.set_selection(value);
- <xsl:text> } else if (index == 2) {
- <xsl:text> const desiredIndices = value.split(":").map((str) => +str);
- <xsl:text> // Cache the original content to prevent data destruction on subsequent filters
- <xsl:text> if (!this.original_content) {
- <xsl:text> this.original_content = [...this.content];
- <xsl:text> this.content = this.original_content.filter((item, idx) => desiredIndices.includes(idx));
- <declarations:DropDownIndexed/>
- <xsl:template match="declarations:DropDownIndexed">
- <xsl:text>/* </xsl:text>
- <xsl:value-of select="local-name()"/>
- <xsl:text> Object.getOwnPropertyNames(DropDownWidget.prototype).forEach(name => {
- <xsl:text> if (name !== "constructor" && name !== "dispatch") {
- <xsl:text> DropDownIndexedWidget.prototype[name] = DropDownWidget.prototype[name];
- <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_defs">
- <xsl:param name="hmi_element"/>
- <xsl:variable name="disability">
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/disabled</xsl:text>
- <xsl:with-param name="mandatory" select="'no'"/>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)>0"/>
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>box button highlight</xsl:text>
- <xsl:variable name="text_elt" select="$hmi_element//*[@inkscape:label='text'][1]"/>
- <xsl:text>init_specific: function() {
- <xsl:when test="count(arg) = 1 and arg[1]/@value = '#langs'">
- <xsl:text> this.text_elt = id("</xsl:text>
- <xsl:value-of select="$text_elt/@id"/>
- <xsl:text> this.content = langs.map(([lname,lcode]) => lname);
- <xsl:when test="count(arg) = 0">
- <xsl:if test="not($text_elt[self::svg:use])">
- <xsl:message terminate="yes">
- <xsl:text>No argument for HMI:DropDownIndexed widget id="</xsl:text>
- <xsl:value-of select="$hmi_element/@id"/>
- <xsl:text>" and "text" labeled element is not a svg:use element</xsl:text>
- <xsl:variable name="real_text_elt" select="$result_widgets[@id = $hmi_element/@id]//*[@original=$text_elt/@id]/svg:text"/>
- <xsl:text> this.text_elt = id("</xsl:text>
- <xsl:value-of select="$real_text_elt/@id"/>
- <xsl:variable name="from_list_id" select="substring-after($text_elt/@xlink:href,'#')"/>
- <xsl:variable name="from_list" select="$hmi_textlists[(@id | */@id) = $from_list_id]"/>
- <xsl:if test="count($from_list) = 0">
- <xsl:message terminate="yes">
- <xsl:text>HMI:DropDownIndexed widget id="</xsl:text>
- <xsl:value-of select="$hmi_element/@id"/>
- <xsl:text>" "text" labeled element does not point to a svg:text owned by a HMI:List widget</xsl:text>
- <xsl:text> this.content = hmi_widgets["</xsl:text>
- <xsl:value-of select="$from_list/@id"/>
- <xsl:text> this.text_elt = id("</xsl:text>
- <xsl:value-of select="$text_elt/@id"/>
- <xsl:text> this.content = [
- <xsl:for-each select="arg">
- <xsl:text> "</xsl:text>
- <xsl:value-of select="@value"/>
- <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis,
- <xsl:text>grid/marks, legends and curves.
- <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing:
- <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels.
- <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be
- <xsl:text> duplicated along axis line to form intervals marks.
- <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their
- <xsl:text> bounding box is the chart wall.
- <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used
- <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths.
- <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored.
- <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text>
- <path name="value" count="1+" accepts="HMI_INT,HMI_REAL">
- <xsl:text>value</xsl:text>
- <arg name="xformat" count="optional" accepts="string">
- <xsl:text>format string for X label</xsl:text>
- <arg name="yformat" count="optional" accepts="string">
- <xsl:text>format string for Y label</xsl:text>
- <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_class">
- <xsl:text>class </xsl:text>
- <xsl:text>HistoryXYGraphWidget</xsl:text>
- <xsl:text> extends Widget{
- <xsl:text> frequency = 1;
- <xsl:text> this.params = [null, null];
- <xsl:text> [this.x_format, this.y_format] = this.args;
- <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this);
- <xsl:text> this.loading = false;
- <xsl:text> this.curves = [];
- <xsl:text> this.curves_data = [];
- <xsl:text> this.init_specific();
- <xsl:text> this.reference = new ReferenceFrame(
- <xsl:text> [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt],
- <xsl:text> [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]],
- <xsl:text> [this.x_axis_label_elt, this.y_axis_label_elt],
- <xsl:text> [this.x_axis_line_elt, this.y_axis_line_elt],
- <xsl:text> [this.x_format, this.y_format]);
- <xsl:text> let max_stroke_width = 0;
- <xsl:text> for (let curve of this.curves) {
- <xsl:text> if (curve.style.strokeWidth > max_stroke_width) {
- <xsl:text> max_stroke_width = curve.style.strokeWidth;
- <xsl:text> this.curves_data.push([]);
- <xsl:text> this.params.push(null);
- <xsl:text> this.Margins = this.reference.getLengths().map(length => max_stroke_width / length);
- <xsl:text> // create <clipPath> path and attach it to widget
- <xsl:text> let clipPath = document.createElementNS(xmlns, "clipPath");
- <xsl:text> let clipPathPath = document.createElementNS(xmlns, "path");
- <xsl:text> let clipPathPathDattr = document.createAttribute("d");
- <xsl:text> clipPathPathDattr.value = this.reference.getClipPathPathDattr();
- <xsl:text> clipPathPath.setAttributeNode(clipPathPathDattr);
- <xsl:text> clipPath.appendChild(clipPathPath);
- <xsl:text> clipPath.id = randomId();
- <xsl:text> this.element.appendChild(clipPath);
- <xsl:text> // assign created clipPath to clip-path property of curves
- <xsl:text> for(let curve of this.curves) {
- <xsl:text> curve.setAttribute("clip-path", "url(#" + clipPath.id + ")");
- <xsl:text> fetch_error(e){
- <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
- <xsl:text> do_http_request() {
- <xsl:text> this.abort_controller = new AbortController();
- <xsl:text> const decoder = new TextDecoder();
- <xsl:text> let partialChunk = '';
- <xsl:text> const query = {
- <xsl:text> startTime: Date.parse(this.params[0]),
- <xsl:text> endTime: Date.parse(this.params[1]),
- <xsl:text> variableNames: this.params.slice(2)
- <xsl:text> const options = {
- <xsl:text> method: 'POST',
- <xsl:text> body: JSON.stringify(query),
- <xsl:text> headers: { 'Content-Type': 'application/json' },
- <xsl:text> signal: this.abort_controller.signal
- <xsl:text> return fetch('/history', options)
- <xsl:text> .then(response => {
- <xsl:text> const reader = response.body.getReader();
- <xsl:text> const read = () => {
- <xsl:text> return reader.read().then(({ value, done }) => {
- <xsl:text> if (done) return;
- <xsl:text> const chunk = decoder.decode(value, { stream: true });
- <xsl:text> const lines = (partialChunk + chunk).split(String.fromCharCode(10));
- <xsl:text> partialChunk = lines.pop();
- <xsl:text> lines.forEach(line => {
- <xsl:text> if (line.trim()) {
- <xsl:text> const row = JSON.parse(line);
- <xsl:text> const vi = query.variableNames.findIndex(v => v === row.varname);
- <xsl:text> if (vi !== -1 && this.curves_data[vi]) {
- <xsl:text> this.curves_data[vi].push([row.timestamp, row.value]);
- <xsl:text> if (row.value > this.ymax) this.ymax = row.value;
- <xsl:text> if (row.value < this.ymin) this.ymin = row.value;
- <xsl:text> return read();
- <xsl:text> return read();
- <xsl:text> }).catch(this.fetch_error_bound);
- <xsl:text> if (this.abort_controller) {
- <xsl:text> this.abort_controller.abort();
- <xsl:text> super.unsub();
- <xsl:text> sub(...args){
- <xsl:text> super.sub(...args);
- <xsl:text> dispatch(value, oldval, index) {
- <xsl:text> this.params[index] = value;
- <xsl:text> if (this.params.every((item) => item !== null)) {
- <xsl:text> if(!this.loading){
- <xsl:text> this.loading = true;
- <xsl:text> this.curves_data = [];
- <xsl:text> for (let curve of this.curves) {
- <xsl:text> this.curves_data.push([]);
- <xsl:text> this.ymin = Infinity;
- <xsl:text> this.ymax = -Infinity;
- <xsl:text> this.do_http_request().finally(() => {
- <xsl:text> let xmin = Infinity;
- <xsl:text> let xmax = -Infinity;
- <xsl:text> let has_data = false;
- <xsl:text> for (let i = 0; i < this.curves.length; i++) {
- <xsl:text> const dataLength = this.curves_data[i].length;
- <xsl:text> if (dataLength > 1) {
- <xsl:text> const ximin = this.curves_data[i][0][0];
- <xsl:text> const ximax = this.curves_data[i][dataLength - 1][0];
- <xsl:text> if (ximin < xmin) xmin = ximin;
- <xsl:text> if (ximax > xmax) xmax = ximax;
- <xsl:text> has_data = true;
- <xsl:text> if (has_data) {
- <xsl:text> this.xmin = xmin;
- <xsl:text> this.xmax = xmax;
- <xsl:text> this.xmin = Date.parse(this.params[0]);
- <xsl:text> this.xmax = Date.parse(this.params[1]);
- <xsl:text> this.ymin = -1;
- <xsl:text> this.ymax = 1;
- <xsl:text> let Xrange = this.xmax - this.xmin;
- <xsl:text> let Yrange = this.ymax - this.ymin;
- <xsl:text> // apply margin by moving min and max to enlarge range
- <xsl:text> let [xMargin, yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m, l]) => m * l);
- <xsl:text> [[this.dxmin, this.dxmax], [this.dymin, this.dymax]] =
- <xsl:text> [[this.xmin - xMargin, this.xmax + xMargin],
- <xsl:text> [this.ymin - yMargin, this.ymax + yMargin]];
- <xsl:text> Xrange += 2 * xMargin;
- <xsl:text> Yrange += 2 * yMargin;
- <xsl:text> // recompute curves "d" attribute
- <xsl:text> let [base_point, xvect, yvect] = this.reference.getBaseRef();
- <xsl:text> this.curves_d_attr =
- <xsl:text> zip(this.curves_data, this.curves).map(([data, curve]) => {
- <xsl:text> let new_d = data.map(([x, y], i) => {
- <xsl:text> // compute curve point from data, ranges, and base_ref
- <xsl:text> let xv = vectorscale(xvect, (x - this.dxmin) / Xrange);
- <xsl:text> let yv = vectorscale(yvect, (y - this.dymin) / Yrange);
- <xsl:text> let px = base_point.x + xv.x + yv.x;
- <xsl:text> let py = base_point.y + xv.y + yv.y;
- <xsl:text> return " " + px + "," + py;
- <xsl:text> new_d.unshift("M ");
- <xsl:text> return new_d.join('');
- <xsl:text> // computed curves "d" attr is applied to svg curve during animate();
- <xsl:text> this.request_animate();
- <xsl:text> this.loading = false;
- <xsl:text> this.reference.applyRanges([[this.dxmin, this.dxmax],
- <xsl:text> [this.dymin, this.dymax]]);
- <xsl:text> // apply computed curves "d" attributes
- <xsl:text> for (let [curve, d_attr] of zip(this.curves, this.curves_d_attr)) {
- <xsl:text> if (d_attr.length > 2)
- <xsl:text> curve.setAttribute("d", d_attr);
- <xsl:text> curve.setAttribute("d", "M 0 0");
- <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_defs">
- <xsl:param name="hmi_element"/>
- <xsl:variable name="disability">
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/disabled</xsl:text>
- <xsl:with-param name="mandatory" select="'no'"/>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)>0"/>
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/x_interval_minor_mark /x_axis_line /x_interval_major_mark /x_axis_label</xsl:text>
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/y_interval_minor_mark /y_axis_line /y_interval_major_mark /y_axis_label</xsl:text>
- <xsl:text> init_specific() {
- <xsl:variable name="curves" select="$hmi_element/*[regexp:test(@inkscape:label,'^curve_[0-9]+$')]"/>
- <xsl:variable name="curves_error" select="func:check_curves_label_consistency($curves,count($curves)-1)"/>
- <xsl:if test="string-length($curves_error)">
- <xsl:message terminate="yes">
- <xsl:text>HistoryXYGraph id="</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>", label="</xsl:text>
- <xsl:value-of select="@inkscape:label"/>
- <xsl:text>" : </xsl:text>
- <xsl:value-of select="$curves_error"/>
- <xsl:for-each select="$curves">
- <xsl:variable name="label" select="@inkscape:label"/>
- <xsl:variable name="_id" select="@id"/>
- <xsl:variable name="curve_num" select="substring(@inkscape:label, 7)"/>
- <xsl:text> this.curves[</xsl:text>
- <xsl:value-of select="$curve_num"/>
- <xsl:text>] = id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>"); /* </xsl:text>
- <xsl:value-of select="@inkscape:label"/>
- <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in
- <xsl:text>SVG sub-elements of "data" labeled element.
- <xsl:text>Documentation to be written. see svghmi example.
- <xsl:text>Http POST variables, spread JSON back</xsl:text>
- <arg name="url" accepts="string"/>
- <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING">
- <xsl:text>single variable to edit</xsl:text>
- <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_class">
- <xsl:text>class </xsl:text>
- <xsl:text>MultiLangJsonTableWidget</xsl:text>
- <xsl:text> extends Widget{
- <xsl:text> // arbitrary defaults to avoid missing entries in query
- <xsl:text> cache = [0,0,0];
- <xsl:text> init_common() {
- <xsl:text> this.spread_json_data_bound = this.spread_json_data.bind(this);
- <xsl:text> this.handle_http_response_bound = this.handle_http_response.bind(this);
- <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this);
- <xsl:text> if (this.should_translate === undefined) {
- <xsl:text> this.should_translate = [];
- <xsl:text> if (this.lang_keys === undefined) {
- <xsl:text> this.lang_keys = [];
- <xsl:text> this.promised = false;
- <xsl:text> handle_http_response(response) {
- <xsl:text> if (!response.ok) {
- <xsl:text> console.log("HTTP error, status = " + response.status);
- <xsl:text> return response.json();
- <xsl:text> fetch_error(e){
- <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
- <xsl:text> do_http_request(...opt) {
- <xsl:text> this.abort_controller = new AbortController();
- <xsl:text> return Promise.resolve().then(() => {
- <xsl:text> const query = {
- <xsl:text> args: this.args,
- <xsl:text> range: this.cache[1],
- <xsl:text> position: this.cache[2],
- <xsl:text> visible: this.visible,
- <xsl:text> extra: this.cache.slice(4),
- <xsl:text> options: opt
- <xsl:text> const options = {
- <xsl:text> method: 'POST',
- <xsl:text> body: JSON.stringify(query),
- <xsl:text> headers: {'Content-Type': 'application/json'},
- <xsl:text> signal: this.abort_controller.signal
- <xsl:text> return fetch(this.args[0], options)
- <xsl:text> .then(this.handle_http_response_bound)
- <xsl:text> .then(this.spread_json_data_bound)
- <xsl:text> .catch(this.fetch_error_bound);
- <xsl:text> this.abort_controller.abort();
- <xsl:text> super.unsub();
- <xsl:text> sub(...args){
- <xsl:text> this.cache[0] = undefined;
- <xsl:text> super.sub(...args);
- <xsl:text> dispatch(value, oldval, index) {
- <xsl:text> if(this.cache[index] != value)
- <xsl:text> this.cache[index] = value;
- <xsl:text> if(!this.promised){
- <xsl:text> this.promised = true;
- <xsl:text> this.do_http_request().finally(() => {
- <xsl:text> this.promised = false;
- <xsl:text> make_on_click(...options){
- <xsl:text> let that = this;
- <xsl:text> return function(evt){
- <xsl:text> that.do_http_request(...options);
- <xsl:text> // on_click(evt, ...options) {
- <xsl:text> // this.do_http_request(...options);
- <xsl:template mode="json_table_elt_render" match="svg:*">
- <xsl:message terminate="yes">
- <xsl:text>MultiLangJsonTable Widget can't contain element of type </xsl:text>
- <xsl:value-of select="local-name()"/>
- <func:function name="func:ml_json_expressions">
- <xsl:param name="expressions"/>
- <xsl:param name="label"/>
- <xsl:when test="$label">
- <xsl:variable name="suffixes" select="str:split($label)"/>
- <xsl:variable name="res">
- <xsl:for-each select="$suffixes">
- <xsl:variable name="suffix" select="."/>
- <xsl:variable name="pos" select="position()"/>
- <xsl:variable name="expr" select="$expressions[position() <= $pos][last()]/expression"/>
- <xsl:if test="$pos = 1">
- <xsl:variable name="raw_selector" select="$suffix"/>
- <xsl:variable name="lang_selector">
- <xsl:when test="starts-with($raw_selector, '.')">
- <xsl:value-of select="substring($raw_selector, 2)"/>
- <xsl:value-of select="$raw_selector"/>
- <xsl:attribute name="lang_selector">
- <xsl:value-of select="$lang_selector"/>
- <xsl:when test="contains($suffix, '=')">
- <xsl:variable name="name" select="substring-before($suffix, '=')"/>
- <xsl:variable name="content_raw" select="substring-after($suffix, '=')"/>
- <xsl:if test="$expr/@name[. != $name]">
- <xsl:message terminate="yes">
- <xsl:text>MultiLangJsonTable : misplaced '=' or inconsistent names in Json data expressions.</xsl:text>
- <xsl:attribute name="name">
- <xsl:value-of select="$name"/>
- <xsl:when test="starts-with($content_raw, '_(') and substring($content_raw, string-length($content_raw)) = ')'">
- <xsl:variable name="raw_key" select="substring($content_raw, 3, string-length($content_raw) - 3)"/>
- <xsl:variable name="clean_key">
- <xsl:when test="starts-with($raw_key, '.')">
- <xsl:value-of select="substring($raw_key, 2)"/>
- <xsl:value-of select="$raw_key"/>
- <xsl:attribute name="translation_key">
- <xsl:value-of select="$clean_key"/>
- <xsl:attribute name="content">
- <xsl:value-of select="$expr/@content"/>
- <xsl:value-of select="$raw_key"/>
- <xsl:attribute name="content">
- <xsl:value-of select="$expr/@content"/>
- <xsl:value-of select="$content_raw"/>
- <xsl:copy-of select="$expr/@name"/>
- <xsl:attribute name="content">
- <xsl:value-of select="$expr/@content"/>
- <xsl:value-of select="$suffix"/>
- <func:result select="exsl:node-set($res)"/>
- <func:result select="$expressions"/>
- <xsl:variable name="ml_initexpr">
- <xsl:attribute name="content">
- <xsl:text>jdata</xsl:text>
- <xsl:variable name="ml_initexpr_ns" select="exsl:node-set($ml_initexpr)"/>
- <xsl:template mode="json_table_elt_render" match="svg:use">
- <xsl:param name="expressions"/>
- <xsl:variable name="targetid" select="substring-after(@xlink:href,'#')"/>
- <xsl:variable name="from_list" select="$hmi_lists[(@id | */@id) = $targetid]"/>
- <xsl:when test="count($from_list) > 0">
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").href.baseVal =
- <xsl:text> "#"+hmi_widgets["</xsl:text>
- <xsl:value-of select="$from_list/@id"/>
- <xsl:text>"].items[</xsl:text>
- <xsl:value-of select="$expressions/expression[1]/@content"/>
- <xsl:message terminate="no">
- <xsl:text>Clones (svg:use) in MultiLangJsonTable Widget must point to a valid HMI:List widget or item. Reference "</xsl:text>
- <xsl:value-of select="@xlink:href"/>
- <xsl:text>" is not valid and will not be updated.</xsl:text>
- <xsl:template mode="json_table_elt_render" match="svg:text">
- <xsl:param name="expressions"/>
- <xsl:variable name="value_expr" select="$expressions/expression[1]/@content"/>
- <xsl:variable name="original" select="@original"/>
- <xsl:variable name="from_textstylelist" select="$textstylelist_related_ns/list[elt/@eltid = $original]"/>
- <xsl:when test="count($from_textstylelist) > 0">
- <xsl:variable name="content_expr" select="$expressions/expression[2]/@content"/>
- <xsl:if test="string-length($content_expr) = 0 or $expressions/expression[2]/@name != 'textContent'">
- <xsl:message terminate="yes">
- <xsl:text>Clones (svg:use) in MultiLangJsonTable Widget pointing to a HMI:TextStyleList widget or item must have a "textContent=.someVal" assignment following value expression in label.</xsl:text>
- <xsl:text> let elt = id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text> elt.textContent = String(</xsl:text>
- <xsl:value-of select="$content_expr"/>
- <xsl:text> elt.style = hmi_widgets["</xsl:text>
- <xsl:value-of select="$from_textstylelist/@listid"/>
- <xsl:text>"].styles[</xsl:text>
- <xsl:value-of select="$value_expr"/>
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").textContent = String(</xsl:text>
- <xsl:value-of select="$value_expr"/>
- <xsl:template mode="json_table_elt_render" match="svg:image">
- <xsl:param name="expressions"/>
- <xsl:variable name="value_expr" select="$expressions/expression[1]/@content"/>
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").setAttribute('href', String(</xsl:text>
- <xsl:value-of select="$value_expr"/>
- <xsl:template mode="json_table_render_except_comments" match="svg:*">
- <xsl:param name="expressions"/>
- <xsl:param name="widget_elts"/>
- <xsl:variable name="label" select="func:filter_non_widget_label(., $widget_elts)"/>
- <xsl:if test="not(starts-with($label,'#'))">
- <xsl:apply-templates mode="json_table_render" select=".">
- <xsl:with-param name="expressions" select="$expressions"/>
- <xsl:with-param name="widget_elts" select="$widget_elts"/>
- <xsl:with-param name="label" select="$label"/>
- <xsl:template mode="json_table_render" match="svg:*">
- <xsl:param name="expressions"/>
- <xsl:param name="widget_elts"/>
- <xsl:param name="label"/>
- <xsl:variable name="new_expressions" select="func:ml_json_expressions($expressions, $label)"/>
- <xsl:variable name="elt" select="."/>
- <xsl:for-each select="$new_expressions/expression[position() > 1][starts-with(@name,'onClick')]">
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="$elt/@id"/>
- <xsl:text>").onclick = this.make_on_click('</xsl:text>
- <xsl:value-of select="@name"/>
- <xsl:text>', </xsl:text>
- <xsl:value-of select="@content"/>
- <xsl:apply-templates mode="json_table_elt_render" select=".">
- <xsl:with-param name="expressions" select="$new_expressions"/>
- <xsl:template mode="json_table_render" match="svg:g">
- <xsl:param name="expressions"/>
- <xsl:param name="widget_elts"/>
- <xsl:param name="label"/>
- <xsl:variable name="varprefix">
- <xsl:text>obj_</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:for-each select="$expressions/expression">
- <xsl:text> let </xsl:text>
- <xsl:value-of select="$varprefix"/>
- <xsl:value-of select="position()"/>
- <xsl:text> = </xsl:text>
- <xsl:value-of select="@content"/>
- <xsl:text> if(</xsl:text>
- <xsl:value-of select="$varprefix"/>
- <xsl:value-of select="position()"/>
- <xsl:text> == undefined) {
- <xsl:variable name="new_expressions">
- <xsl:for-each select="$expressions/expression">
- <xsl:copy-of select="@name"/>
- <xsl:attribute name="content">
- <xsl:value-of select="$varprefix"/>
- <xsl:value-of select="position()"/>
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").style = "</xsl:text>
- <xsl:value-of select="@style"/>
- <xsl:apply-templates mode="json_table_render_except_comments" select="*">
- <xsl:with-param name="expressions" select="func:ml_json_expressions(exsl:node-set($new_expressions), $label)"/>
- <xsl:with-param name="widget_elts" select="$widget_elts"/>
- <xsl:text> } catch(err) {
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").style = "display:none";
- <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_defs">
- <xsl:param name="hmi_element"/>
- <xsl:variable name="disability">
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/disabled</xsl:text>
- <xsl:with-param name="mandatory" select="'no'"/>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)>0"/>
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>data</xsl:text>
- <xsl:variable name="data_elt" select="$result_svg_ns//*[@id = $hmi_element/@id]/*[@inkscape:label = 'data']"/>
- <xsl:variable name="widget_elts" select="$hmi_element/*[@inkscape:label = 'data']/descendant::svg:*"/>
- <xsl:variable name="all_parsed_expressions">
- <xsl:for-each select="$data_elt/descendant-or-self::svg:*[@inkscape:label]">
- <xsl:variable name="exprs" select="func:ml_json_expressions($ml_initexpr_ns, @inkscape:label)"/>
- <xsl:copy-of select="exsl:node-set($exprs)//expression[@translation_key or @lang_selector]"/>
- <xsl:variable name="all_exprs_ns" select="exsl:node-set($all_parsed_expressions)"/>
- <xsl:text> visible: </xsl:text>
- <xsl:value-of select="count($data_elt/*[@inkscape:label])"/>
- <xsl:text> should_translate: [
- <xsl:for-each select="$all_exprs_ns/expression[@translation_key]">
- <xsl:if test="not(@translation_key = preceding-sibling::expression/@translation_key)">
- <xsl:text> "</xsl:text>
- <xsl:value-of select="@translation_key"/>
- <xsl:if test="position() != last()">
- <xsl:text> lang_keys: [
- <xsl:for-each select="$all_exprs_ns/expression[@lang_selector]">
- <xsl:if test="not(@lang_selector = preceding-sibling::expression/@lang_selector)">
- <xsl:text> "</xsl:text>
- <xsl:value-of select="@lang_selector"/>
- <xsl:if test="position() != last()">
- <xsl:text> spread_json_data: function(janswer) {
- <xsl:text> let [range,position,jdata] = janswer;
- <xsl:text> if (jdata.length > 0 && this.should_translate.length > 0) {
- <xsl:text> const lang = cache[lang_local_index];
- <xsl:text> const langcode = langs[lang][1];
- <xsl:text> for (let row of jdata) {
- <xsl:text> for (const key of this.should_translate) {
- <xsl:text> if (key in row) {
- <xsl:text> const orig = row[key];
- <xsl:text> const match = translations.find(item => item[1][0] == orig);
- <xsl:text> const tr = match ? match[1][lang] : orig;
- <xsl:text> row[key] = tr;
- <xsl:text> for (const key of this.lang_keys) {
- <xsl:text> if (key in row) {
- <xsl:text> row[key] = String(row[key]) + "_" + langcode;
- <xsl:text> [[1, range], [2, position], [3, this.visible]].map(([i,v]) => {
- <xsl:text> this.apply_hmi_value(i,v);
- <xsl:text> this.cache[i] = v;
- <xsl:apply-templates mode="json_table_render_except_comments" select="$data_elt">
- <xsl:with-param name="expressions" select="$ml_initexpr_ns"/>
- <xsl:with-param name="widget_elts" select="$widget_elts"/>
- <xsl:text> this.init_common();
- <xsl:for-each select="$hmi_element/*[starts-with(@inkscape:label,'action_')]">
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").onclick = this.make_on_click("</xsl:text>
- <xsl:value-of select="func:escape_quotes(@inkscape:label)"/>
- <xsl:for-each select="$hmi_element/*[starts-with(@inkscape:label,'dict')]">
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").style.display = "none";
- <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
- <xsl:value-of select="@type"/>
- <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes
- <xsl:text>associated actions. The widget should be placed on top of the area where the
- <xsl:text>movement should be detected. It is a group containing a graphical element
- <xsl:text>"area" which defines the area where the swipe should be detected.
- <xsl:text>For each of the motions to be detected there must exist several parameters
- <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set:
- <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents
- <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of
- <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to
- <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set
- <xsl:text>to). change and set commands should also be accompanied by paths with the
- <xsl:text>same name and their values should be variable names to apply the command to.
- <xsl:text>Additional parameters to add are:
- <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer
- <xsl:text> movement. Anything below that value will not be considered
- <xsl:text> a movement. If omitted, 5 will be used.
- <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer
- <xsl:text> down event. If time elapses without any significant movement
- <xsl:text> (defined by movethreshold), the pointer down/click event
- <xsl:text> will be propagated on an element on a lower level than the
- <xsl:text> swiping area. Similar thing will happen on pointer up event
- <xsl:text> if there was no significant movement.
- <xsl:text>HMI:Swipe:movethreshold=3:left_xthreshold=30:left_ythreshold=5:left_jump=Home:up_xthreshold=25:up_ythreshold=5:up_change=+2@up_change=/VAR0
- <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must
- <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget
- <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect
- <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and
- <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2.
- <xsl:text>Detect swipe motion and react accordingly</xsl:text>
- <xsl:template match="widget[@type='Swipe']" mode="widget_class">
- <xsl:text>class </xsl:text>
- <xsl:text>SwipeWidget</xsl:text>
- <xsl:text> extends Widget{
- <xsl:text> frequency = 2;
- <xsl:text> startX = -1;
- <xsl:text> startY = -1;
- <xsl:text> moveThreshold = 5;
- <xsl:text> pressTimeout = 300;
- <xsl:text> touchTimer = null;
- <xsl:text> settings = {
- <xsl:text> actions: [],
- <xsl:text> xThreshold: 100,
- <xsl:text> yThreshold: 0,
- <xsl:text> actions: [],
- <xsl:text> xThreshold: 100,
- <xsl:text> yThreshold: 0,
- <xsl:text> actions: [],
- <xsl:text> xThreshold: 0,
- <xsl:text> yThreshold: 100,
- <xsl:text> actions: [],
- <xsl:text> xThreshold: 0,
- <xsl:text> yThreshold: 100,
- <xsl:text> propagateMouseDownEvent(simulateUp) {
- <xsl:text> const elements = document.elementsFromPoint(this.startX, this.startY);
- <xsl:text> if (elements.length > 1) {
- <xsl:text> const eventDown = new MouseEvent("pointerdown", {
- <xsl:text> view: window,
- <xsl:text> bubbles: true,
- <xsl:text> cancelable: true,
- <xsl:text> const eventClick = new MouseEvent("click", {
- <xsl:text> view: window,
- <xsl:text> bubbles: true,
- <xsl:text> cancelable: true,
- <xsl:text> const eventUp = new MouseEvent("pointerup", {
- <xsl:text> view: window,
- <xsl:text> bubbles: true,
- <xsl:text> cancelable: true,
- <xsl:text> const cb = document.getElementById(elements[1].id);
- <xsl:text> cb.dispatchEvent(eventDown);
- <xsl:text> cb.dispatchEvent(eventClick);
- <xsl:text> if (simulateUp) {
- <xsl:text> window.setTimeout(() => {
- <xsl:text> cb.dispatchEvent(eventUp);
- <xsl:text> onMouseUp(evt) {
- <xsl:text> window.clearTimeout(this.touchTimer);
- <xsl:text> this.touchTimer = null;
- <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true);
- <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true);
- <xsl:text> const area = this.element.getBoundingClientRect();
- <xsl:text> var xDiff = (evt.pageX - this.startX) * 100.0 / area.width;
- <xsl:text> var yDiff = (evt.pageY - this.startY) * 100.0 / area.height;
- <xsl:text> var action = null;
- <xsl:text> if (xDiff < 0 && Math.abs(xDiff) >= this.settings.left.xThreshold && Math.abs(yDiff) < this.settings.left.yThreshold) {
- <xsl:text> action = "left";
- <xsl:text> } else if (xDiff > 0 && Math.abs(xDiff) >= this.settings.right.xThreshold && Math.abs(yDiff) < this.settings.right.yThreshold) {
- <xsl:text> action = "right";
- <xsl:text> } else if (yDiff < 0 && Math.abs(yDiff) >= this.settings.up.yThreshold && Math.abs(xDiff) < this.settings.up.xThreshold) {
- <xsl:text> action = "up";
- <xsl:text> } else if (yDiff > 0 && Math.abs(yDiff) >= this.settings.down.yThreshold && Math.abs(xDiff) < this.settings.down.xThreshold) {
- <xsl:text> action = "down";
- <xsl:text> } else if (Math.abs(xDiff) < this.moveThreshold && Math.abs(yDiff) < this.moveThreshold) {
- <xsl:text> this.propagateMouseDownEvent(true);
- <xsl:text> if (action) {
- <xsl:text> for (var a of this.settings[action].actions) {
- <xsl:text> if (a.action == "jump") {
- <xsl:text> fading_page_switch(a.target);
- <xsl:text> } else if (a.action == "change") {
- <xsl:text> this.change_hmi_value(a.var_idx, a.value);
- <xsl:text> } else if (a.action == "set") {
- <xsl:text> this.apply_hmi_value(a.var_idx, a.value);
- <xsl:text> onMouseMove(evt) {
- <xsl:text> this.currX = evt.pageX;
- <xsl:text> this.currY = evt.pageY;
- <xsl:text> onMouseDown(evt) {
- <xsl:text> this.startX = evt.pageX;
- <xsl:text> this.startY = evt.pageY;
- <xsl:text> this.currX = evt.pageX;
- <xsl:text> this.currY = evt.pageY;
- <xsl:text> svg_root.addEventListener("pointerup", this.boundOnMouseUp, true);
- <xsl:text> svg_root.addEventListener("pointermove", this.boundOnMouseMove, true);
- <xsl:text> this.touchTimer = window.setTimeout(() => {
- <xsl:text> const area = this.element.getBBox();
- <xsl:text> var xDiff = (this.currX - this.startX) * 100.0 / area.width;
- <xsl:text> var yDiff = (this.currY - this.startY) * 100.0 / area.height;
- <xsl:text> if (Math.abs(xDiff) < this.moveThreshold && Math.abs(yDiff) < this.moveThreshold) {
- <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true);
- <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true);
- <xsl:text> this.propagateMouseDownEvent(false);
- <xsl:text> this.touchTimer = null;
- <xsl:text> }, this.pressTimeout);
- <xsl:text> this.request_animate();
- <xsl:template match="widget[@type='Swipe']" mode="widget_defs">
- <xsl:param name="hmi_element"/>
- <xsl:variable name="disability">
- <xsl:call-template name="defs_by_labels">
- <xsl:with-param name="hmi_element" select="$hmi_element"/>
- <xsl:with-param name="labels">
- <xsl:text>/disabled</xsl:text>
- <xsl:with-param name="mandatory" select="'no'"/>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)>0"/>
- <xsl:text> init: function() {
- <xsl:text> this.boundOnMouseUp = this.onMouseUp.bind(this);
- <xsl:text> this.boundOnMouseMove = this.onMouseMove.bind(this);
- <xsl:text> this.element.addEventListener("pointerdown", this.onMouseDown.bind(this));
- <xsl:text> const dirs = ["left", "right", "up", "down"];
- <xsl:text> var properDir = false;
- <xsl:text> var pathIndex = -1;
- <xsl:variable name="paths" select="path"/>
- <xsl:for-each select="arg[contains(@value, '=')]">
- <xsl:variable name="name" select="substring-before(@value, '=')"/>
- <xsl:variable name="value" select="substring-after(@value, '=')"/>
- <xsl:variable name="index">
- <xsl:for-each select="$paths">
- <xsl:if test="@assign = $name">
- <xsl:value-of select="position()-1"/>
- <xsl:variable name="direction" select="substring-before($name, '_')"/>
- <xsl:variable name="command" select="substring-after($name, '_')"/>
- <xsl:text> if ("</xsl:text>
- <xsl:value-of select="$index"/>
- <xsl:text>".length > 0) {
- <xsl:text> pathIndex = Number("</xsl:text>
- <xsl:value-of select="$index"/>
- <xsl:text> properDir = dirs.findIndex((x) => x == "</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text> if (properDir) {
- <xsl:text> switch ("</xsl:text>
- <xsl:value-of select="$command"/>
- <xsl:text> case "xthreshold":
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].xThreshold = </xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text> case "ythreshold":
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].yThreshold = </xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text> case "jump":
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].actions.push({
- <xsl:text> action: "jump",
- <xsl:text> target: "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text> case "change":
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].actions.push({
- <xsl:text> action: "change",
- <xsl:text> var_idx: pathIndex,
- <xsl:text> value: "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].actions.push({
- <xsl:text> action: "set",
- <xsl:text> var_idx: pathIndex,
- <xsl:text> value: "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text> if ("</xsl:text>
- <xsl:value-of select="$name"/>
- <xsl:text>" == "movethreshold") {
- <xsl:text> this.moveThreshold = "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text> } else if ("</xsl:text>
- <xsl:value-of select="$name"/>
- <xsl:text>" == "presstimeout") {
- <xsl:text> this.pressTimeout = "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:template match="widget[@type='VarSync']" mode="widget_class">
- <xsl:text>class </xsl:text>
- <xsl:text>VarSyncWidget</xsl:text>
- <xsl:text> extends Widget{
- <xsl:text> dispatch(value, oldval, varnum) {
- <xsl:text> if (varnum === 0) {
- <xsl:text> let dest_index = this.get_variable_index(1);
- <xsl:text> let current_dest_val = cache[dest_index];
- <xsl:text> if (value !== current_dest_val) {
- <xsl:text> this.apply_hmi_value(1, value);
- <xsl:text> else if (varnum === 1) {
- <xsl:text> let src_index = this.get_variable_index(0);
- <xsl:text> if (value !== cache[src_index]) {
- <xsl:text> this.apply_hmi_value(0, value);
- <xsl:text> this.element.style.display = "none";
<xsl:text>Made with SVGHMI. https://beremiz.org</xsl:text>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LPCSVGHMI/widget_touchdetect.ysl2 Tue May 12 14:06:42 2026 +0200
@@ -0,0 +1,50 @@
+// widget_touchdetect.ysl2 — global overlay; place the group as a direct child of the root +// svg alongside HMI:Page groups so it is not removed on page changes. +widget_desc("TouchDetect") { + TouchDetect publishes whether any finger is on the screen via one HMI variable (first path). + Place the group under the document svg as a sibling of page groups (it is not tied to any page's + widget list). init() sets relativeness/offset so get_variable_index works without calling sub() + (sub() registers with subscribers and runs apply_cache; not needed for outbound-only touch writes). + Label example: HMI:TouchDetect@/YourBoolOrInt — while at least one touch pointer is active the + variable is 1, otherwise 0. Listeners are on document.body (pointerdown/pointerup/pointercancel). + shortdesc > Global touch active flag (body pointer events, touch pointers only) + path name="touch_active" count="1" accepts="HMI_BOOL,HMI_INT" +widget_class("TouchDetect") { + const n = this.indexes ? this.indexes.length : 0; + this.relativeness = Array(n).fill(false); + this.container_id = ""; + this._touchPointerIds = new Set(); + this._onPointerDown = (e) => { + if (this._touchPointerIds.size === 0) { + this.apply_hmi_value(0, 1); + this._touchPointerIds.add(e.pointerId); + this._onPointerUp = (e) => { + this._touchPointerIds.delete(e.pointerId); + if (this._touchPointerIds.size === 0) { + this.apply_hmi_value(0, 0); + document.body.addEventListener("pointerdown", this._onPointerDown, { passive: true }); + document.body.addEventListener("pointerup", this._onPointerUp, { passive: true }); + document.body.addEventListener("pointercancel", this._onPointerUp, { passive: true }); +widget_defs("TouchDetect") { + error > TouchDetect id="«@id»": exactly one HMI path is required (e.g. HMI:TouchDetect@/PLC_TOUCH_ACTIVE)