lpcmanager

Parents 34827d0071d3
Children
Create new widget - HMI:TouchDetect that can be added as page sibling to detect screen touches independently of screen saver and set PLC variable accordingly
--- 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 @@
#! gmake
+# 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
+# is included.
+
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
+
all: $(xsltfiles)
$(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:template>
<xsl:variable name="hmi_elements" select="//svg:*[starts-with(@inkscape:label, 'HMI:')]"/>
+ <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label
+</xsl:text>
+ <xsl:text>HMI:AnimateRotation:optional_args
+</xsl:text>
+ <xsl:text>Element to rotate is a part of that group labeled "animate".
+</xsl:text>
+ <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right",
+</xsl:text>
+ <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element
+</xsl:text>
+ <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Optional arguments are:
+</xsl:text>
+ <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set)
+</xsl:text>
+ <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set)
+</xsl:text>
+ <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used)
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>The higher the frame rate, the higher CPU usage will be.
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Rotation animation</xsl:text>
+ </shortdesc>
+ </xsl:template>
+ <xsl:template match="widget[@type='CloudImage']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by
+</xsl:text>
+ <xsl:text>link to the file whose name is the value of given variable, being served from
+</xsl:text>
+ <xsl:text>/media/data/cloud folder.
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Image display</xsl:text>
+ </shortdesc>
+ </xsl:template>
+ <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>DropDownIndexed widget can have one, two or three path variables.
+</xsl:text>
+ <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text),
+</xsl:text>
+ <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect)
+</xsl:text>
+ <xsl:text>labeled elements.
+</xsl:text>
+ <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the
+</xsl:text>
+ <xsl:text>limit of available space in page, and "box" is extended to contain all
+</xsl:text>
+ <xsl:text>texts.
+</xsl:text>
+ <xsl:text>"highlight" is moved over pre-selected entry.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>The first variable path is index of selection, and the second is value of selection.
+</xsl:text>
+ <xsl:text>In case there are one or two path variables, a list of texts is defined via
+</xsl:text>
+ <xsl:text>arguments.
+</xsl:text>
+ <xsl:text>If there are no arguments, it is expected that "text" labeled element is of
+</xsl:text>
+ <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget.
+</xsl:text>
+ <xsl:text>In that case list of texts is set to TextList content.
+</xsl:text>
+ <xsl:text>When only one argument is given and its value is "#langs" then list of
+</xsl:text>
+ <xsl:text>texts is automatically set to the human-readable list of supported
+</xsl:text>
+ <xsl:text>languages by this HMI.
+</xsl:text>
+ <xsl:text>Otherwise, arguments are used as dropdown options.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>In case there are three path variables, the third path variable is a filter
+</xsl:text>
+ <xsl:text>in a form of a string containing ':' separated list of indices of the options
+</xsl:text>
+ <xsl:text>from the arguments that will be shown in the dropdown.
+</xsl:text>
+ <xsl:text>Examples:
+</xsl:text>
+ <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE
+</xsl:text>
+ <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Let user select text entry in a drop-down menu</xsl:text>
+ </shortdesc>
+ <arg name="entries" count="many" accepts="string">
+ <xsl:text>drop-down menu entries</xsl:text>
+ </arg>
+ <path name="selected_index" accepts="HMI_INT">
+ <xsl:text>selection index</xsl:text>
+ </path>
+ <path name="selected_value" accepts="HMI_STRING">
+ <xsl:text>selection value</xsl:text>
+ </path>
+ <path name="filter" accepts="HMI_STRING">
+ <xsl:text>indices of shown drop-down menu entries</xsl:text>
+ </path>
+ </xsl:template>
+ <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis,
+</xsl:text>
+ <xsl:text>grid/marks, legends and curves.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing:
+</xsl:text>
+ <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels.
+</xsl:text>
+ <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be
+</xsl:text>
+ <xsl:text> duplicated along axis line to form intervals marks.
+</xsl:text>
+ <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their
+</xsl:text>
+ <xsl:text> bounding box is the chart wall.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used
+</xsl:text>
+ <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths.
+</xsl:text>
+ <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text>
+ </shortdesc>
+ <path name="value" count="1+" accepts="HMI_INT,HMI_REAL">
+ <xsl:text>value</xsl:text>
+ </path>
+ <arg name="xformat" count="optional" accepts="string">
+ <xsl:text>format string for X label</xsl:text>
+ </arg>
+ <arg name="yformat" count="optional" accepts="string">
+ <xsl:text>format string for Y label</xsl:text>
+ </arg>
+ </xsl:template>
+ <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in
+</xsl:text>
+ <xsl:text>SVG sub-elements of "data" labeled element.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Documentation to be written. see svghmi example.
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Http POST variables, spread JSON back</xsl:text>
+ </shortdesc>
+ <arg name="url" accepts="string"/>
+ <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING">
+ <xsl:text>single variable to edit</xsl:text>
+ </path>
+ </xsl:template>
+ <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes
+</xsl:text>
+ <xsl:text>associated actions. The widget should be placed on top of the area where the
+</xsl:text>
+ <xsl:text>movement should be detected. It is a group containing a graphical element
+</xsl:text>
+ <xsl:text>"area" which defines the area where the swipe should be detected.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>For each of the motions to be detected there must exist several parameters
+</xsl:text>
+ <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set:
+</xsl:text>
+ <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents
+</xsl:text>
+ <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of
+</xsl:text>
+ <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to
+</xsl:text>
+ <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set
+</xsl:text>
+ <xsl:text>to). change and set commands should also be accompanied by paths with the
+</xsl:text>
+ <xsl:text>same name and their values should be variable names to apply the command to.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Additional parameters to add are:
+</xsl:text>
+ <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer
+</xsl:text>
+ <xsl:text> movement. Anything below that value will not be considered
+</xsl:text>
+ <xsl:text> a movement. If omitted, 5 will be used.
+</xsl:text>
+ <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer
+</xsl:text>
+ <xsl:text> down event. If time elapses without any significant movement
+</xsl:text>
+ <xsl:text> (defined by movethreshold), the pointer down/click event
+</xsl:text>
+ <xsl:text> will be propagated on an element on a lower level than the
+</xsl:text>
+ <xsl:text> swiping area. Similar thing will happen on pointer up event
+</xsl:text>
+ <xsl:text> if there was no significant movement.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Examples:
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <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>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must
+</xsl:text>
+ <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget
+</xsl:text>
+ <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect
+</xsl:text>
+ <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and
+</xsl:text>
+ <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Detect swipe motion and react accordingly</xsl:text>
+ </shortdesc>
+ </xsl:template>
+ <xsl:template match="widget[@type='TouchDetect']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>TouchDetect publishes whether any finger is on the screen via one HMI variable (first path).
+</xsl:text>
+ <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>
+ <xsl:text>widget list). init() sets relativeness/offset so get_variable_index works without calling sub()
+</xsl:text>
+ <xsl:text>(sub() registers with subscribers and runs apply_cache; not needed for outbound-only touch writes).
+</xsl:text>
+ <xsl:text>Label example: HMI:TouchDetect@/YourBoolOrInt &#x2014; while at least one touch pointer is active the
+</xsl:text>
+ <xsl:text>variable is 1, otherwise 0. Listeners are on document.body (pointerdown/pointerup/pointercancel).
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Global touch active flag (body pointer events, touch pointers only)</xsl:text>
+ </shortdesc>
+ <path name="touch_active" count="1" accepts="HMI_BOOL,HMI_INT"/>
+ </xsl:template>
<xsl:template match="widget[@type='Assign']" mode="widget_desc">
<type>
<xsl:value-of select="@type"/>
@@ -1110,281 +1408,6 @@
</xsl:variable>
<func:result select="$res"/>
</func:function>
- <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label
-</xsl:text>
- <xsl:text>HMI:AnimateRotation:optional_args
-</xsl:text>
- <xsl:text>Element to rotate is a part of that group labeled "animate".
-</xsl:text>
- <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right",
-</xsl:text>
- <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element
-</xsl:text>
- <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Optional arguments are:
-</xsl:text>
- <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set)
-</xsl:text>
- <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set)
-</xsl:text>
- <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used)
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>The higher the frame rate, the higher CPU usage will be.
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Rotation animation</xsl:text>
- </shortdesc>
- </xsl:template>
- <xsl:template match="widget[@type='CloudImage']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by
-</xsl:text>
- <xsl:text>link to the file whose name is the value of given variable, being served from
-</xsl:text>
- <xsl:text>/media/data/cloud folder.
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Image display</xsl:text>
- </shortdesc>
- </xsl:template>
- <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>DropDownIndexed widget can have one, two or three path variables.
-</xsl:text>
- <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text),
-</xsl:text>
- <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect)
-</xsl:text>
- <xsl:text>labeled elements.
-</xsl:text>
- <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the
-</xsl:text>
- <xsl:text>limit of available space in page, and "box" is extended to contain all
-</xsl:text>
- <xsl:text>texts.
-</xsl:text>
- <xsl:text>"highlight" is moved over pre-selected entry.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>The first variable path is index of selection, and the second is value of selection.
-</xsl:text>
- <xsl:text>In case there are one or two path variables, a list of texts is defined via
-</xsl:text>
- <xsl:text>arguments.
-</xsl:text>
- <xsl:text>If there are no arguments, it is expected that "text" labeled element is of
-</xsl:text>
- <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget.
-</xsl:text>
- <xsl:text>In that case list of texts is set to TextList content.
-</xsl:text>
- <xsl:text>When only one argument is given and its value is "#langs" then list of
-</xsl:text>
- <xsl:text>texts is automatically set to the human-readable list of supported
-</xsl:text>
- <xsl:text>languages by this HMI.
-</xsl:text>
- <xsl:text>Otherwise, arguments are used as dropdown options.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>In case there are three path variables, the third path variable is a filter
-</xsl:text>
- <xsl:text>in a form of a string containing ':' separated list of indices of the options
-</xsl:text>
- <xsl:text>from the arguments that will be shown in the dropdown.
-</xsl:text>
- <xsl:text>Examples:
-</xsl:text>
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE
-</xsl:text>
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Let user select text entry in a drop-down menu</xsl:text>
- </shortdesc>
- <arg name="entries" count="many" accepts="string">
- <xsl:text>drop-down menu entries</xsl:text>
- </arg>
- <path name="selected_index" accepts="HMI_INT">
- <xsl:text>selection index</xsl:text>
- </path>
- <path name="selected_value" accepts="HMI_STRING">
- <xsl:text>selection value</xsl:text>
- </path>
- <path name="filter" accepts="HMI_STRING">
- <xsl:text>indices of shown drop-down menu entries</xsl:text>
- </path>
- </xsl:template>
- <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis,
-</xsl:text>
- <xsl:text>grid/marks, legends and curves.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing:
-</xsl:text>
- <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels.
-</xsl:text>
- <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be
-</xsl:text>
- <xsl:text> duplicated along axis line to form intervals marks.
-</xsl:text>
- <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their
-</xsl:text>
- <xsl:text> bounding box is the chart wall.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used
-</xsl:text>
- <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths.
-</xsl:text>
- <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text>
- </shortdesc>
- <path name="value" count="1+" accepts="HMI_INT,HMI_REAL">
- <xsl:text>value</xsl:text>
- </path>
- <arg name="xformat" count="optional" accepts="string">
- <xsl:text>format string for X label</xsl:text>
- </arg>
- <arg name="yformat" count="optional" accepts="string">
- <xsl:text>format string for Y label</xsl:text>
- </arg>
- </xsl:template>
- <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in
-</xsl:text>
- <xsl:text>SVG sub-elements of "data" labeled element.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Documentation to be written. see svghmi example.
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Http POST variables, spread JSON back</xsl:text>
- </shortdesc>
- <arg name="url" accepts="string"/>
- <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING">
- <xsl:text>single variable to edit</xsl:text>
- </path>
- </xsl:template>
- <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes
-</xsl:text>
- <xsl:text>associated actions. The widget should be placed on top of the area where the
-</xsl:text>
- <xsl:text>movement should be detected. It is a group containing a graphical element
-</xsl:text>
- <xsl:text>"area" which defines the area where the swipe should be detected.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>For each of the motions to be detected there must exist several parameters
-</xsl:text>
- <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set:
-</xsl:text>
- <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents
-</xsl:text>
- <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of
-</xsl:text>
- <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to
-</xsl:text>
- <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set
-</xsl:text>
- <xsl:text>to). change and set commands should also be accompanied by paths with the
-</xsl:text>
- <xsl:text>same name and their values should be variable names to apply the command to.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Additional parameters to add are:
-</xsl:text>
- <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer
-</xsl:text>
- <xsl:text> movement. Anything below that value will not be considered
-</xsl:text>
- <xsl:text> a movement. If omitted, 5 will be used.
-</xsl:text>
- <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer
-</xsl:text>
- <xsl:text> down event. If time elapses without any significant movement
-</xsl:text>
- <xsl:text> (defined by movethreshold), the pointer down/click event
-</xsl:text>
- <xsl:text> will be propagated on an element on a lower level than the
-</xsl:text>
- <xsl:text> swiping area. Similar thing will happen on pointer up event
-</xsl:text>
- <xsl:text> if there was no significant movement.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Examples:
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <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>
- <xsl:text>
-</xsl:text>
- <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must
-</xsl:text>
- <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget
-</xsl:text>
- <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect
-</xsl:text>
- <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and
-</xsl:text>
- <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Detect swipe motion and react accordingly</xsl:text>
- </shortdesc>
- </xsl:template>
<xsl:template mode="document" match="@* | node()">
<xsl:copy>
<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" {
+ choose {
+ when "$hmi_pages_descs/arg[1]/@value = 'Home'" > Home
+ otherwise {
+ 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']";
+ const "delay" choose {
+ when "$screensaverpage" {
+ const "delaystr", "$screensaverpage/arg[2]/@value";
+ if "not(regexp:test($delaystr,'^[0-9]+$'))"
+ error > ScreenSaver page has missing or malformed delay argument.
+ value "$delaystr";
+ }
+ otherwise > null
+ }
+ | 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" {
+ param "elems";
+ const "descend", "$elems/descendant-or-self::svg:*";
+ const "clones", "$descend[self::svg:use]";
+ const "originals", "//svg:*[concat('#',@id) = $clones/@xlink:href]";
+ choose {
+ when "$originals"
+ result "$descend | func:refered_elements($originals)";
+ otherwise
+ result "$descend";
+ }
+}
+
+// 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))";
+ elt {
+ attrib "id" > «@id»
+ copy "func:overlapping_geometry(.)";
+ }
+ value "ns:ProgressEnd($k)";
+ }
+}
+
+const "overlapping_geometry", "exsl:node-set($_overlapping_geometry)";
+
+def "func:all_related_elements" {
+ param "page";
+ 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" {
+ param "pages";
+ choose{
+ when "$pages"{
+ result """func:all_related_elements($pages[1])
+ | func:required_elements($pages[position()!=1])""";
+ }otherwise{
+ result "/..";
+ }
+ }
+}
+
+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" {
+ param "elements";
+ const "short_list", "$elements[not(ancestor::*/@id = $elements/@id)]";
+ const "filled_groups", """$short_list/parent::svg:g[
+ not(child::*[
+ 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" {
+ param "pages";
+ choose{
+ when "$pages"{
+ result """func:sumarized_elements(func:all_related_elements($pages[1]))
+ | func:detachable_elements($pages[position()!=1])""";
+ }otherwise{
+ result "/..";
+ }
+ }
+}
+
+// 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 "page", ".";
+ 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",
+ """$sumarized_page/
+ ancestor-or-self::*[@id = $detachable_elements/@id]""";
+
+ | "«$pagename»": {
+ | 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»",
+ }
+ | widgets: [
+ | [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()" > ,`
+ }
+ | ],
+ | jumps: [
+ 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" {
+ |
+ | var page_desc = {
+ apply "$hmi_pages", mode="page_desc";
+ | }
+}
+
+template "*", mode="widget_page";
+
+
+emit "debug:detachable-pages" {
+ |
+ | DETACHABLES:
+ foreach "$detachable_elements"{
+ | «@id»
+ }
+ | DISCARDABLES:
+ foreach "$discardable_elements"{
+ | «@id»
+ }
+ | In ForEach:
+ foreach "$in_forEach_widget_ids"{
+ | «.»
+ }
+ | Overlapping
+ 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:template>
<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 @@
</func:function>
<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:choose>
</func:function>
<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">
<xsl:text>
@@ -944,7 +946,7 @@
<xsl:text>
</xsl:text>
</xsl:for-each>
- <xsl:text>In Foreach:
+ <xsl:text>In ForEach:
</xsl:text>
<xsl:for-each select="$in_forEach_widget_ids">
<xsl:text> </xsl:text>
@@ -2605,6 +2607,2240 @@
</xsl:otherwise>
</xsl:choose>
</func:function>
+ <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label
+</xsl:text>
+ <xsl:text>HMI:AnimateRotation:optional_args
+</xsl:text>
+ <xsl:text>Element to rotate is a part of that group labeled "animate".
+</xsl:text>
+ <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right",
+</xsl:text>
+ <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element
+</xsl:text>
+ <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Optional arguments are:
+</xsl:text>
+ <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set)
+</xsl:text>
+ <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set)
+</xsl:text>
+ <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used)
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>The higher the frame rate, the higher CPU usage will be.
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Rotation animation</xsl:text>
+ </shortdesc>
+ </xsl:template>
+ <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>
+ <xsl:text> duration = 2000;
+</xsl:text>
+ <xsl:text> iterations = "infinite";
+</xsl:text>
+ <xsl:text> center_x = null;
+</xsl:text>
+ <xsl:text> center_y = null;
+</xsl:text>
+ <xsl:text> frame_rate = 10;
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <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>
+ <xsl:with-param name="mandatory" select="'no'"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:value-of select="$disability"/>
+ <xsl:variable name="has_disability" select="string-length($disability)&gt;0"/>
+ <xsl:text> init() {
+</xsl:text>
+ <xsl:variable name="widget_type" select="@type"/>
+ <xsl:variable name="widget_id" select="@id"/>
+ <xsl:text> const widget_pos = this.element.getBBox();
+</xsl:text>
+ <xsl:text> this.center_x = widget_pos.x + widget_pos.width / 2;
+</xsl:text>
+ <xsl:text> this.center_y = widget_pos.y + widget_pos.height / 2;
+</xsl:text>
+ <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:text>;
+</xsl:text>
+ </xsl:for-each>
+ <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:text>");
+</xsl:text>
+ <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>";
+</xsl:text>
+ <xsl:text> const el_pos = el.getBBox();
+</xsl:text>
+ <xsl:text> switch (el_label) {
+</xsl:text>
+ <xsl:text> case "top_left":
+</xsl:text>
+ <xsl:text> this.center_x = el_pos.x;
+</xsl:text>
+ <xsl:text> this.center_y = el_pos.y;
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "top_right":
+</xsl:text>
+ <xsl:text> this.center_x = el_pos.x + el_pos.width;
+</xsl:text>
+ <xsl:text> this.center_y = el_pos.y;
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "bottom_left":
+</xsl:text>
+ <xsl:text> this.center_x = el_pos.x;
+</xsl:text>
+ <xsl:text> this.center_y = el_pos.y + el_pos.height;
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "bottom_right":
+</xsl:text>
+ <xsl:text> this.center_x = el_pos.x + el_pos.width;
+</xsl:text>
+ <xsl:text> this.center_y = el_pos.y + el_pos.height;
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "center":
+</xsl:text>
+ <xsl:text> this.center_x = el_pos.x + el_pos.width / 2;
+</xsl:text>
+ <xsl:text> this.center_y = el_pos.y + el_pos.height / 2;
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this.element.removeChild(el);
+</xsl:text>
+ </xsl:if>
+ <xsl:if test="count($center_element) &gt; 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:variable>
+ <xsl:message terminate="yes">
+ <xsl:value-of select="$errmsg"/>
+ </xsl:message>
+ </xsl:if>
+ <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:variable>
+ <xsl:message terminate="yes">
+ <xsl:value-of select="$errmsg"/>
+ </xsl:message>
+ </xsl:if>
+ <xsl:text> var anim_el = id("</xsl:text>
+ <xsl:value-of select="$animate_element/@id"/>
+ <xsl:text>");
+</xsl:text>
+ <xsl:text> anim_el.style.transformOrigin = vsprintf("%.2fpx %.2fpx", [this.center_x, this.center_y]);
+</xsl:text>
+ <xsl:text> anim_el.style.animation = vsprintf("animateRotation %.3fs steps(%s) %s", [this.duration / 1000.0, this.frame_rate, this.iterations]);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ </xsl:template>
+ <cssdefs:animaterotation/>
+ <xsl:template match="cssdefs:animaterotation">
+ <xsl:text>
+</xsl:text>
+ <xsl:text>/* </xsl:text>
+ <xsl:value-of select="local-name()"/>
+ <xsl:text> */
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>@keyframes animateRotation {
+</xsl:text>
+ <xsl:text> 100% { transform: rotate(360deg); }
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ </xsl:template>
+ <xsl:template match="widget[@type='CloudImage']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by
+</xsl:text>
+ <xsl:text>link to the file whose name is the value of given variable, being served from
+</xsl:text>
+ <xsl:text>/media/data/cloud folder.
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Image display</xsl:text>
+ </shortdesc>
+ </xsl:template>
+ <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>
+ <xsl:text> frequency = 5;
+</xsl:text>
+ <xsl:text> dispatch(value, oldval, index) {
+</xsl:text>
+ <xsl:text> if (index == 0) {
+</xsl:text>
+ <xsl:text> this.given_url = "cloudfolder?image=" + value;
+</xsl:text>
+ <xsl:text> this.ready = true;
+</xsl:text>
+ <xsl:text> this.request_animate();
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <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>
+ <xsl:with-param name="mandatory" select="'no'"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:value-of select="$disability"/>
+ <xsl:variable name="has_disability" select="string-length($disability)&gt;0"/>
+ <xsl:text> given_url: "",
+</xsl:text>
+ <xsl:text> ready: false,
+</xsl:text>
+ <xsl:text> animate: function(){
+</xsl:text>
+ <xsl:text> this.element.setAttribute('href', this.given_url);
+</xsl:text>
+ <xsl:text> },
+</xsl:text>
+ </xsl:template>
+ <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:image[starts-with(@inkscape:label, 'HMI:CloudImage')]">
+ <xsl:copy>
+ <xsl:apply-templates mode="inline_svg" select="@*[not(contains(name(), 'href'))] | node()"/>
+ </xsl:copy>
+ </xsl:template>
+ <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>DropDownIndexed widget can have one, two or three path variables.
+</xsl:text>
+ <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text),
+</xsl:text>
+ <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect)
+</xsl:text>
+ <xsl:text>labeled elements.
+</xsl:text>
+ <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the
+</xsl:text>
+ <xsl:text>limit of available space in page, and "box" is extended to contain all
+</xsl:text>
+ <xsl:text>texts.
+</xsl:text>
+ <xsl:text>"highlight" is moved over pre-selected entry.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>The first variable path is index of selection, and the second is value of selection.
+</xsl:text>
+ <xsl:text>In case there are one or two path variables, a list of texts is defined via
+</xsl:text>
+ <xsl:text>arguments.
+</xsl:text>
+ <xsl:text>If there are no arguments, it is expected that "text" labeled element is of
+</xsl:text>
+ <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget.
+</xsl:text>
+ <xsl:text>In that case list of texts is set to TextList content.
+</xsl:text>
+ <xsl:text>When only one argument is given and its value is "#langs" then list of
+</xsl:text>
+ <xsl:text>texts is automatically set to the human-readable list of supported
+</xsl:text>
+ <xsl:text>languages by this HMI.
+</xsl:text>
+ <xsl:text>Otherwise, arguments are used as dropdown options.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>In case there are three path variables, the third path variable is a filter
+</xsl:text>
+ <xsl:text>in a form of a string containing ':' separated list of indices of the options
+</xsl:text>
+ <xsl:text>from the arguments that will be shown in the dropdown.
+</xsl:text>
+ <xsl:text>Examples:
+</xsl:text>
+ <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE
+</xsl:text>
+ <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Let user select text entry in a drop-down menu</xsl:text>
+ </shortdesc>
+ <arg name="entries" count="many" accepts="string">
+ <xsl:text>drop-down menu entries</xsl:text>
+ </arg>
+ <path name="selected_index" accepts="HMI_INT">
+ <xsl:text>selection index</xsl:text>
+ </path>
+ <path name="selected_value" accepts="HMI_STRING">
+ <xsl:text>selection value</xsl:text>
+ </path>
+ <path name="filter" accepts="HMI_STRING">
+ <xsl:text>indices of shown drop-down menu entries</xsl:text>
+ </path>
+ </xsl:template>
+ <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>
+ <xsl:text> dispatch(value, old_val, index) {
+</xsl:text>
+ <xsl:text> if (index == 0) {
+</xsl:text>
+ <xsl:text> if (!this.opened) this.set_selection(value);
+</xsl:text>
+ <xsl:text> } else if (index == 2) {
+</xsl:text>
+ <xsl:text> try {
+</xsl:text>
+ <xsl:text> const desiredIndices = value.split(":").map((str) =&gt; +str);
+</xsl:text>
+ <xsl:text> // Cache the original content to prevent data destruction on subsequent filters
+</xsl:text>
+ <xsl:text> if (!this.original_content) {
+</xsl:text>
+ <xsl:text> this.original_content = [...this.content];
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this.content = this.original_content.filter((item, idx) =&gt; desiredIndices.includes(idx));
+</xsl:text>
+ <xsl:text> } catch {}
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <declarations:DropDownIndexed/>
+ <xsl:template match="declarations:DropDownIndexed">
+ <xsl:text>
+</xsl:text>
+ <xsl:text>/* </xsl:text>
+ <xsl:value-of select="local-name()"/>
+ <xsl:text> */
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> Object.getOwnPropertyNames(DropDownWidget.prototype).forEach(name =&gt; {
+</xsl:text>
+ <xsl:text> if (name !== "constructor" &amp;&amp; name !== "dispatch") {
+</xsl:text>
+ <xsl:text> DropDownIndexedWidget.prototype[name] = DropDownWidget.prototype[name];
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ </xsl:template>
+ <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>
+ <xsl:with-param name="mandatory" select="'no'"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:value-of select="$disability"/>
+ <xsl:variable name="has_disability" select="string-length($disability)&gt;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:with-param>
+ </xsl:call-template>
+ <xsl:variable name="text_elt" select="$hmi_element//*[@inkscape:label='text'][1]"/>
+ <xsl:text>init_specific: function() {
+</xsl:text>
+ <xsl:choose>
+ <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>");
+</xsl:text>
+ <xsl:text> this.content = langs.map(([lname,lcode]) =&gt; lname);
+</xsl:text>
+ </xsl:when>
+ <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:message>
+ </xsl:if>
+ <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:text>");
+</xsl:text>
+ <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:message>
+ </xsl:if>
+ <xsl:text> this.content = hmi_widgets["</xsl:text>
+ <xsl:value-of select="$from_list/@id"/>
+ <xsl:text>"].texts;
+</xsl:text>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:text> this.text_elt = id("</xsl:text>
+ <xsl:value-of select="$text_elt/@id"/>
+ <xsl:text>");
+</xsl:text>
+ <xsl:text> this.content = [
+</xsl:text>
+ <xsl:for-each select="arg">
+ <xsl:text> "</xsl:text>
+ <xsl:value-of select="@value"/>
+ <xsl:text>",
+</xsl:text>
+ </xsl:for-each>
+ <xsl:text> ];
+</xsl:text>
+ </xsl:otherwise>
+ </xsl:choose>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis,
+</xsl:text>
+ <xsl:text>grid/marks, legends and curves.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing:
+</xsl:text>
+ <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels.
+</xsl:text>
+ <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be
+</xsl:text>
+ <xsl:text> duplicated along axis line to form intervals marks.
+</xsl:text>
+ <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their
+</xsl:text>
+ <xsl:text> bounding box is the chart wall.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used
+</xsl:text>
+ <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths.
+</xsl:text>
+ <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text>
+ </shortdesc>
+ <path name="value" count="1+" accepts="HMI_INT,HMI_REAL">
+ <xsl:text>value</xsl:text>
+ </path>
+ <arg name="xformat" count="optional" accepts="string">
+ <xsl:text>format string for X label</xsl:text>
+ </arg>
+ <arg name="yformat" count="optional" accepts="string">
+ <xsl:text>format string for Y label</xsl:text>
+ </arg>
+ </xsl:template>
+ <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>
+ <xsl:text> frequency = 1;
+</xsl:text>
+ <xsl:text> init() {
+</xsl:text>
+ <xsl:text> this.params = [null, null];
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> [this.x_format, this.y_format] = this.args;
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this);
+</xsl:text>
+ <xsl:text> this.loading = false;
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> this.curves = [];
+</xsl:text>
+ <xsl:text> this.curves_data = [];
+</xsl:text>
+ <xsl:text> this.init_specific();
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> this.reference = new ReferenceFrame(
+</xsl:text>
+ <xsl:text> [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt],
+</xsl:text>
+ <xsl:text> [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]],
+</xsl:text>
+ <xsl:text> [this.x_axis_label_elt, this.y_axis_label_elt],
+</xsl:text>
+ <xsl:text> [this.x_axis_line_elt, this.y_axis_line_elt],
+</xsl:text>
+ <xsl:text> [this.x_format, this.y_format]);
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> let max_stroke_width = 0;
+</xsl:text>
+ <xsl:text> for (let curve of this.curves) {
+</xsl:text>
+ <xsl:text> if (curve.style.strokeWidth &gt; max_stroke_width) {
+</xsl:text>
+ <xsl:text> max_stroke_width = curve.style.strokeWidth;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this.curves_data.push([]);
+</xsl:text>
+ <xsl:text> this.params.push(null);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> this.Margins = this.reference.getLengths().map(length =&gt; max_stroke_width / length);
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> // create &lt;clipPath&gt; path and attach it to widget
+</xsl:text>
+ <xsl:text> let clipPath = document.createElementNS(xmlns, "clipPath");
+</xsl:text>
+ <xsl:text> let clipPathPath = document.createElementNS(xmlns, "path");
+</xsl:text>
+ <xsl:text> let clipPathPathDattr = document.createAttribute("d");
+</xsl:text>
+ <xsl:text> clipPathPathDattr.value = this.reference.getClipPathPathDattr();
+</xsl:text>
+ <xsl:text> clipPathPath.setAttributeNode(clipPathPathDattr);
+</xsl:text>
+ <xsl:text> clipPath.appendChild(clipPathPath);
+</xsl:text>
+ <xsl:text> clipPath.id = randomId();
+</xsl:text>
+ <xsl:text> this.element.appendChild(clipPath);
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> // assign created clipPath to clip-path property of curves
+</xsl:text>
+ <xsl:text> for(let curve of this.curves) {
+</xsl:text>
+ <xsl:text> curve.setAttribute("clip-path", "url(#" + clipPath.id + ")");
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> fetch_error(e){
+</xsl:text>
+ <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> do_http_request() {
+</xsl:text>
+ <xsl:text> this.abort_controller = new AbortController();
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const decoder = new TextDecoder();
+</xsl:text>
+ <xsl:text> let partialChunk = '';
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const query = {
+</xsl:text>
+ <xsl:text> startTime: Date.parse(this.params[0]),
+</xsl:text>
+ <xsl:text> endTime: Date.parse(this.params[1]),
+</xsl:text>
+ <xsl:text> variableNames: this.params.slice(2)
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const options = {
+</xsl:text>
+ <xsl:text> method: 'POST',
+</xsl:text>
+ <xsl:text> body: JSON.stringify(query),
+</xsl:text>
+ <xsl:text> headers: { 'Content-Type': 'application/json' },
+</xsl:text>
+ <xsl:text> signal: this.abort_controller.signal
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> return fetch('/history', options)
+</xsl:text>
+ <xsl:text> .then(response =&gt; {
+</xsl:text>
+ <xsl:text> const reader = response.body.getReader();
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const read = () =&gt; {
+</xsl:text>
+ <xsl:text> return reader.read().then(({ value, done }) =&gt; {
+</xsl:text>
+ <xsl:text> if (done) return;
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const chunk = decoder.decode(value, { stream: true });
+</xsl:text>
+ <xsl:text> const lines = (partialChunk + chunk).split(String.fromCharCode(10));
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> partialChunk = lines.pop();
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> lines.forEach(line =&gt; {
+</xsl:text>
+ <xsl:text> if (line.trim()) {
+</xsl:text>
+ <xsl:text> const row = JSON.parse(line);
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const vi = query.variableNames.findIndex(v =&gt; v === row.varname);
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> if (vi !== -1 &amp;&amp; this.curves_data[vi]) {
+</xsl:text>
+ <xsl:text> this.curves_data[vi].push([row.timestamp, row.value]);
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> if (row.value &gt; this.ymax) this.ymax = row.value;
+</xsl:text>
+ <xsl:text> if (row.value &lt; this.ymin) this.ymin = row.value;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> return read();
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> return read();
+</xsl:text>
+ <xsl:text> }).catch(this.fetch_error_bound);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> unsub() {
+</xsl:text>
+ <xsl:text> if (this.abort_controller) {
+</xsl:text>
+ <xsl:text> this.abort_controller.abort();
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> super.unsub();
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> sub(...args){
+</xsl:text>
+ <xsl:text> super.sub(...args);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> dispatch(value, oldval, index) {
+</xsl:text>
+ <xsl:text> this.params[index] = value;
+</xsl:text>
+ <xsl:text> if (this.params.every((item) =&gt; item !== null)) {
+</xsl:text>
+ <xsl:text> if(!this.loading){
+</xsl:text>
+ <xsl:text> this.loading = true;
+</xsl:text>
+ <xsl:text> this.curves_data = [];
+</xsl:text>
+ <xsl:text> for (let curve of this.curves) {
+</xsl:text>
+ <xsl:text> this.curves_data.push([]);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this.ymin = Infinity;
+</xsl:text>
+ <xsl:text> this.ymax = -Infinity;
+</xsl:text>
+ <xsl:text> this.do_http_request().finally(() =&gt; {
+</xsl:text>
+ <xsl:text> let xmin = Infinity;
+</xsl:text>
+ <xsl:text> let xmax = -Infinity;
+</xsl:text>
+ <xsl:text> let has_data = false;
+</xsl:text>
+ <xsl:text> for (let i = 0; i &lt; this.curves.length; i++) {
+</xsl:text>
+ <xsl:text> const dataLength = this.curves_data[i].length;
+</xsl:text>
+ <xsl:text> if (dataLength &gt; 1) {
+</xsl:text>
+ <xsl:text> const ximin = this.curves_data[i][0][0];
+</xsl:text>
+ <xsl:text> const ximax = this.curves_data[i][dataLength - 1][0];
+</xsl:text>
+ <xsl:text> if (ximin &lt; xmin) xmin = ximin;
+</xsl:text>
+ <xsl:text> if (ximax &gt; xmax) xmax = ximax;
+</xsl:text>
+ <xsl:text> has_data = true;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> if (has_data) {
+</xsl:text>
+ <xsl:text> this.xmin = xmin;
+</xsl:text>
+ <xsl:text> this.xmax = xmax;
+</xsl:text>
+ <xsl:text> } else {
+</xsl:text>
+ <xsl:text> this.xmin = Date.parse(this.params[0]);
+</xsl:text>
+ <xsl:text> this.xmax = Date.parse(this.params[1]);
+</xsl:text>
+ <xsl:text> this.ymin = -1;
+</xsl:text>
+ <xsl:text> this.ymax = 1;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> let Xrange = this.xmax - this.xmin;
+</xsl:text>
+ <xsl:text> let Yrange = this.ymax - this.ymin;
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> // apply margin by moving min and max to enlarge range
+</xsl:text>
+ <xsl:text> let [xMargin, yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m, l]) =&gt; m * l);
+</xsl:text>
+ <xsl:text> [[this.dxmin, this.dxmax], [this.dymin, this.dymax]] =
+</xsl:text>
+ <xsl:text> [[this.xmin - xMargin, this.xmax + xMargin],
+</xsl:text>
+ <xsl:text> [this.ymin - yMargin, this.ymax + yMargin]];
+</xsl:text>
+ <xsl:text> Xrange += 2 * xMargin;
+</xsl:text>
+ <xsl:text> Yrange += 2 * yMargin;
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> // recompute curves "d" attribute
+</xsl:text>
+ <xsl:text> let [base_point, xvect, yvect] = this.reference.getBaseRef();
+</xsl:text>
+ <xsl:text> this.curves_d_attr =
+</xsl:text>
+ <xsl:text> zip(this.curves_data, this.curves).map(([data, curve]) =&gt; {
+</xsl:text>
+ <xsl:text> let new_d = data.map(([x, y], i) =&gt; {
+</xsl:text>
+ <xsl:text> // compute curve point from data, ranges, and base_ref
+</xsl:text>
+ <xsl:text> let xv = vectorscale(xvect, (x - this.dxmin) / Xrange);
+</xsl:text>
+ <xsl:text> let yv = vectorscale(yvect, (y - this.dymin) / Yrange);
+</xsl:text>
+ <xsl:text> let px = base_point.x + xv.x + yv.x;
+</xsl:text>
+ <xsl:text> let py = base_point.y + xv.y + yv.y;
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> return " " + px + "," + py;
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> new_d.unshift("M ");
+</xsl:text>
+ <xsl:text> return new_d.join('');
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> // computed curves "d" attr is applied to svg curve during animate();
+</xsl:text>
+ <xsl:text> this.request_animate();
+</xsl:text>
+ <xsl:text> this.loading = false;
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> animate() {
+</xsl:text>
+ <xsl:text> this.reference.applyRanges([[this.dxmin, this.dxmax],
+</xsl:text>
+ <xsl:text> [this.dymin, this.dymax]]);
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> // apply computed curves "d" attributes
+</xsl:text>
+ <xsl:text> for (let [curve, d_attr] of zip(this.curves, this.curves_d_attr)) {
+</xsl:text>
+ <xsl:text> if (d_attr.length &gt; 2)
+</xsl:text>
+ <xsl:text> curve.setAttribute("d", d_attr);
+</xsl:text>
+ <xsl:text> else
+</xsl:text>
+ <xsl:text> curve.setAttribute("d", "M 0 0");
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <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>
+ <xsl:with-param name="mandatory" select="'no'"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:value-of select="$disability"/>
+ <xsl:variable name="has_disability" select="string-length($disability)&gt;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:with-param>
+ </xsl:call-template>
+ <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:with-param>
+ </xsl:call-template>
+ <xsl:text> init_specific() {
+</xsl:text>
+ <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:message>
+ </xsl:if>
+ <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:text> */
+</xsl:text>
+ </xsl:for-each>
+ <xsl:text> }
+</xsl:text>
+ </xsl:template>
+ <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in
+</xsl:text>
+ <xsl:text>SVG sub-elements of "data" labeled element.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Documentation to be written. see svghmi example.
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Http POST variables, spread JSON back</xsl:text>
+ </shortdesc>
+ <arg name="url" accepts="string"/>
+ <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING">
+ <xsl:text>single variable to edit</xsl:text>
+ </path>
+ </xsl:template>
+ <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>
+ <xsl:text> // arbitrary defaults to avoid missing entries in query
+</xsl:text>
+ <xsl:text> cache = [0,0,0];
+</xsl:text>
+ <xsl:text> init_common() {
+</xsl:text>
+ <xsl:text> this.spread_json_data_bound = this.spread_json_data.bind(this);
+</xsl:text>
+ <xsl:text> this.handle_http_response_bound = this.handle_http_response.bind(this);
+</xsl:text>
+ <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this);
+</xsl:text>
+ <xsl:text> if (this.should_translate === undefined) {
+</xsl:text>
+ <xsl:text> this.should_translate = [];
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> if (this.lang_keys === undefined) {
+</xsl:text>
+ <xsl:text> this.lang_keys = [];
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this.promised = false;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> handle_http_response(response) {
+</xsl:text>
+ <xsl:text> if (!response.ok) {
+</xsl:text>
+ <xsl:text> console.log("HTTP error, status = " + response.status);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> return response.json();
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> fetch_error(e){
+</xsl:text>
+ <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> do_http_request(...opt) {
+</xsl:text>
+ <xsl:text> this.abort_controller = new AbortController();
+</xsl:text>
+ <xsl:text> return Promise.resolve().then(() =&gt; {
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const query = {
+</xsl:text>
+ <xsl:text> args: this.args,
+</xsl:text>
+ <xsl:text> range: this.cache[1],
+</xsl:text>
+ <xsl:text> position: this.cache[2],
+</xsl:text>
+ <xsl:text> visible: this.visible,
+</xsl:text>
+ <xsl:text> extra: this.cache.slice(4),
+</xsl:text>
+ <xsl:text> options: opt
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> const options = {
+</xsl:text>
+ <xsl:text> method: 'POST',
+</xsl:text>
+ <xsl:text> body: JSON.stringify(query),
+</xsl:text>
+ <xsl:text> headers: {'Content-Type': 'application/json'},
+</xsl:text>
+ <xsl:text> signal: this.abort_controller.signal
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> return fetch(this.args[0], options)
+</xsl:text>
+ <xsl:text> .then(this.handle_http_response_bound)
+</xsl:text>
+ <xsl:text> .then(this.spread_json_data_bound)
+</xsl:text>
+ <xsl:text> .catch(this.fetch_error_bound);
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> unsub(){
+</xsl:text>
+ <xsl:text> this.abort_controller.abort();
+</xsl:text>
+ <xsl:text> super.unsub();
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> sub(...args){
+</xsl:text>
+ <xsl:text> this.cache[0] = undefined;
+</xsl:text>
+ <xsl:text> super.sub(...args);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> dispatch(value, oldval, index) {
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> if(this.cache[index] != value)
+</xsl:text>
+ <xsl:text> this.cache[index] = value;
+</xsl:text>
+ <xsl:text> else
+</xsl:text>
+ <xsl:text> return;
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> if(!this.promised){
+</xsl:text>
+ <xsl:text> this.promised = true;
+</xsl:text>
+ <xsl:text> this.do_http_request().finally(() =&gt; {
+</xsl:text>
+ <xsl:text> this.promised = false;
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> make_on_click(...options){
+</xsl:text>
+ <xsl:text> let that = this;
+</xsl:text>
+ <xsl:text> return function(evt){
+</xsl:text>
+ <xsl:text> that.do_http_request(...options);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> // on_click(evt, ...options) {
+</xsl:text>
+ <xsl:text> // this.do_http_request(...options);
+</xsl:text>
+ <xsl:text> // }
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <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()"/>
+ <xsl:text>.</xsl:text>
+ </xsl:message>
+ </xsl:template>
+ <func:function name="func:ml_json_expressions">
+ <xsl:param name="expressions"/>
+ <xsl:param name="label"/>
+ <xsl:choose>
+ <xsl:when test="$label">
+ <xsl:variable name="suffixes" select="str:split($label)"/>
+ <xsl:variable name="res">
+ <xsl:for-each select="$suffixes">
+ <expression>
+ <xsl:variable name="suffix" select="."/>
+ <xsl:variable name="pos" select="position()"/>
+ <xsl:variable name="expr" select="$expressions[position() &lt;= $pos][last()]/expression"/>
+ <xsl:if test="$pos = 1">
+ <xsl:variable name="raw_selector" select="$suffix"/>
+ <xsl:variable name="lang_selector">
+ <xsl:choose>
+ <xsl:when test="starts-with($raw_selector, '.')">
+ <xsl:value-of select="substring($raw_selector, 2)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="$raw_selector"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+ <xsl:attribute name="lang_selector">
+ <xsl:value-of select="$lang_selector"/>
+ </xsl:attribute>
+ </xsl:if>
+ <xsl:choose>
+ <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:message>
+ </xsl:if>
+ <xsl:attribute name="name">
+ <xsl:value-of select="$name"/>
+ </xsl:attribute>
+ <xsl:choose>
+ <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:choose>
+ <xsl:when test="starts-with($raw_key, '.')">
+ <xsl:value-of select="substring($raw_key, 2)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="$raw_key"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+ <xsl:attribute name="translation_key">
+ <xsl:value-of select="$clean_key"/>
+ </xsl:attribute>
+ <xsl:attribute name="content">
+ <xsl:value-of select="$expr/@content"/>
+ <xsl:value-of select="$raw_key"/>
+ </xsl:attribute>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:attribute name="content">
+ <xsl:value-of select="$expr/@content"/>
+ <xsl:value-of select="$content_raw"/>
+ </xsl:attribute>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:copy-of select="$expr/@name"/>
+ <xsl:attribute name="content">
+ <xsl:value-of select="$expr/@content"/>
+ <xsl:value-of select="$suffix"/>
+ </xsl:attribute>
+ </xsl:otherwise>
+ </xsl:choose>
+ </expression>
+ </xsl:for-each>
+ </xsl:variable>
+ <func:result select="exsl:node-set($res)"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <func:result select="$expressions"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </func:function>
+ <xsl:variable name="ml_initexpr">
+ <expression>
+ <xsl:attribute name="content">
+ <xsl:text>jdata</xsl:text>
+ </xsl:attribute>
+ </expression>
+ </xsl:variable>
+ <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:choose>
+ <xsl:when test="count($from_list) &gt; 0">
+ <xsl:text> id("</xsl:text>
+ <xsl:value-of select="@id"/>
+ <xsl:text>").href.baseVal =
+</xsl:text>
+ <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:text>];
+</xsl:text>
+ </xsl:when>
+ <xsl:otherwise>
+ <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:message>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+ <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:choose>
+ <xsl:when test="count($from_textstylelist) &gt; 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:message>
+ </xsl:if>
+ <xsl:text> {
+</xsl:text>
+ <xsl:text> let elt = id("</xsl:text>
+ <xsl:value-of select="@id"/>
+ <xsl:text>");
+</xsl:text>
+ <xsl:text> elt.textContent = String(</xsl:text>
+ <xsl:value-of select="$content_expr"/>
+ <xsl:text>);
+</xsl:text>
+ <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>];
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:text> id("</xsl:text>
+ <xsl:value-of select="@id"/>
+ <xsl:text>").textContent = String(</xsl:text>
+ <xsl:value-of select="$value_expr"/>
+ <xsl:text>);
+</xsl:text>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+ <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:text>));
+</xsl:text>
+ </xsl:template>
+ <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:apply-templates>
+ </xsl:if>
+ </xsl:template>
+ <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() &gt; 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:text>);
+</xsl:text>
+ </xsl:for-each>
+ <xsl:apply-templates mode="json_table_elt_render" select=".">
+ <xsl:with-param name="expressions" select="$new_expressions"/>
+ </xsl:apply-templates>
+ </xsl:template>
+ <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:text>_</xsl:text>
+ </xsl:variable>
+ <xsl:text> try {
+</xsl:text>
+ <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>;
+</xsl:text>
+ <xsl:text> if(</xsl:text>
+ <xsl:value-of select="$varprefix"/>
+ <xsl:value-of select="position()"/>
+ <xsl:text> == undefined) {
+</xsl:text>
+ <xsl:text> throw null;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ </xsl:for-each>
+ <xsl:variable name="new_expressions">
+ <xsl:for-each select="$expressions/expression">
+ <xsl:copy>
+ <xsl:copy-of select="@name"/>
+ <xsl:attribute name="content">
+ <xsl:value-of select="$varprefix"/>
+ <xsl:value-of select="position()"/>
+ </xsl:attribute>
+ </xsl:copy>
+ </xsl:for-each>
+ </xsl:variable>
+ <xsl:text> id("</xsl:text>
+ <xsl:value-of select="@id"/>
+ <xsl:text>").style = "</xsl:text>
+ <xsl:value-of select="@style"/>
+ <xsl:text>";
+</xsl:text>
+ <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:apply-templates>
+ <xsl:text> } catch(err) {
+</xsl:text>
+ <xsl:text> id("</xsl:text>
+ <xsl:value-of select="@id"/>
+ <xsl:text>").style = "display:none";
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ </xsl:template>
+ <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>
+ <xsl:with-param name="mandatory" select="'no'"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:value-of select="$disability"/>
+ <xsl:variable name="has_disability" select="string-length($disability)&gt;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:with-param>
+ </xsl:call-template>
+ <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:for-each>
+ </xsl:variable>
+ <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>,
+</xsl:text>
+ <xsl:text> should_translate: [
+</xsl:text>
+ <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:text>"
+</xsl:text>
+ <xsl:if test="position() != last()">
+ <xsl:text>,
+</xsl:text>
+ </xsl:if>
+ </xsl:if>
+ </xsl:for-each>
+ <xsl:text> ],
+</xsl:text>
+ <xsl:text> lang_keys: [
+</xsl:text>
+ <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:text>"
+</xsl:text>
+ <xsl:if test="position() != last()">
+ <xsl:text>,
+</xsl:text>
+ </xsl:if>
+ </xsl:if>
+ </xsl:for-each>
+ <xsl:text> ],
+</xsl:text>
+ <xsl:text> spread_json_data: function(janswer) {
+</xsl:text>
+ <xsl:text> let [range,position,jdata] = janswer;
+</xsl:text>
+ <xsl:text> if (jdata.length &gt; 0 &amp;&amp; this.should_translate.length &gt; 0) {
+</xsl:text>
+ <xsl:text> const lang = cache[lang_local_index];
+</xsl:text>
+ <xsl:text> const langcode = langs[lang][1];
+</xsl:text>
+ <xsl:text> for (let row of jdata) {
+</xsl:text>
+ <xsl:text> for (const key of this.should_translate) {
+</xsl:text>
+ <xsl:text> if (key in row) {
+</xsl:text>
+ <xsl:text> const orig = row[key];
+</xsl:text>
+ <xsl:text> const match = translations.find(item =&gt; item[1][0] == orig);
+</xsl:text>
+ <xsl:text> const tr = match ? match[1][lang] : orig;
+</xsl:text>
+ <xsl:text> row[key] = tr;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> for (const key of this.lang_keys) {
+</xsl:text>
+ <xsl:text> if (key in row) {
+</xsl:text>
+ <xsl:text> row[key] = String(row[key]) + "_" + langcode;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> [[1, range], [2, position], [3, this.visible]].map(([i,v]) =&gt; {
+</xsl:text>
+ <xsl:text> this.apply_hmi_value(i,v);
+</xsl:text>
+ <xsl:text> this.cache[i] = v;
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <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:apply-templates>
+ <xsl:text> },
+</xsl:text>
+ <xsl:text> init() {
+</xsl:text>
+ <xsl:text> this.init_common();
+</xsl:text>
+ <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:text>");
+</xsl:text>
+ </xsl:for-each>
+ <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:text>
+ </xsl:for-each>
+ <xsl:text> }
+</xsl:text>
+ </xsl:template>
+ <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes
+</xsl:text>
+ <xsl:text>associated actions. The widget should be placed on top of the area where the
+</xsl:text>
+ <xsl:text>movement should be detected. It is a group containing a graphical element
+</xsl:text>
+ <xsl:text>"area" which defines the area where the swipe should be detected.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>For each of the motions to be detected there must exist several parameters
+</xsl:text>
+ <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set:
+</xsl:text>
+ <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents
+</xsl:text>
+ <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of
+</xsl:text>
+ <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to
+</xsl:text>
+ <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set
+</xsl:text>
+ <xsl:text>to). change and set commands should also be accompanied by paths with the
+</xsl:text>
+ <xsl:text>same name and their values should be variable names to apply the command to.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Additional parameters to add are:
+</xsl:text>
+ <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer
+</xsl:text>
+ <xsl:text> movement. Anything below that value will not be considered
+</xsl:text>
+ <xsl:text> a movement. If omitted, 5 will be used.
+</xsl:text>
+ <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer
+</xsl:text>
+ <xsl:text> down event. If time elapses without any significant movement
+</xsl:text>
+ <xsl:text> (defined by movethreshold), the pointer down/click event
+</xsl:text>
+ <xsl:text> will be propagated on an element on a lower level than the
+</xsl:text>
+ <xsl:text> swiping area. Similar thing will happen on pointer up event
+</xsl:text>
+ <xsl:text> if there was no significant movement.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>Examples:
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <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>
+ <xsl:text>
+</xsl:text>
+ <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must
+</xsl:text>
+ <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget
+</xsl:text>
+ <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect
+</xsl:text>
+ <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and
+</xsl:text>
+ <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2.
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Detect swipe motion and react accordingly</xsl:text>
+ </shortdesc>
+ </xsl:template>
+ <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>
+ <xsl:text> frequency = 2;
+</xsl:text>
+ <xsl:text> startX = -1;
+</xsl:text>
+ <xsl:text> startY = -1;
+</xsl:text>
+ <xsl:text> currX = -1;
+</xsl:text>
+ <xsl:text> currY = -1;
+</xsl:text>
+ <xsl:text> moveThreshold = 5;
+</xsl:text>
+ <xsl:text> pressTimeout = 300;
+</xsl:text>
+ <xsl:text> touchTimer = null;
+</xsl:text>
+ <xsl:text> settings = {
+</xsl:text>
+ <xsl:text> left: {
+</xsl:text>
+ <xsl:text> actions: [],
+</xsl:text>
+ <xsl:text> xThreshold: 100,
+</xsl:text>
+ <xsl:text> yThreshold: 0,
+</xsl:text>
+ <xsl:text> },
+</xsl:text>
+ <xsl:text> right: {
+</xsl:text>
+ <xsl:text> actions: [],
+</xsl:text>
+ <xsl:text> xThreshold: 100,
+</xsl:text>
+ <xsl:text> yThreshold: 0,
+</xsl:text>
+ <xsl:text> },
+</xsl:text>
+ <xsl:text> up: {
+</xsl:text>
+ <xsl:text> actions: [],
+</xsl:text>
+ <xsl:text> xThreshold: 0,
+</xsl:text>
+ <xsl:text> yThreshold: 100,
+</xsl:text>
+ <xsl:text> },
+</xsl:text>
+ <xsl:text> down: {
+</xsl:text>
+ <xsl:text> actions: [],
+</xsl:text>
+ <xsl:text> xThreshold: 0,
+</xsl:text>
+ <xsl:text> yThreshold: 100,
+</xsl:text>
+ <xsl:text> },
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> propagateMouseDownEvent(simulateUp) {
+</xsl:text>
+ <xsl:text> const elements = document.elementsFromPoint(this.startX, this.startY);
+</xsl:text>
+ <xsl:text> if (elements.length &gt; 1) {
+</xsl:text>
+ <xsl:text> const eventDown = new MouseEvent("pointerdown", {
+</xsl:text>
+ <xsl:text> view: window,
+</xsl:text>
+ <xsl:text> bubbles: true,
+</xsl:text>
+ <xsl:text> cancelable: true,
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> const eventClick = new MouseEvent("click", {
+</xsl:text>
+ <xsl:text> view: window,
+</xsl:text>
+ <xsl:text> bubbles: true,
+</xsl:text>
+ <xsl:text> cancelable: true,
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> const eventUp = new MouseEvent("pointerup", {
+</xsl:text>
+ <xsl:text> view: window,
+</xsl:text>
+ <xsl:text> bubbles: true,
+</xsl:text>
+ <xsl:text> cancelable: true,
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> const cb = document.getElementById(elements[1].id);
+</xsl:text>
+ <xsl:text> cb.dispatchEvent(eventDown);
+</xsl:text>
+ <xsl:text> cb.dispatchEvent(eventClick);
+</xsl:text>
+ <xsl:text> if (simulateUp) {
+</xsl:text>
+ <xsl:text> window.setTimeout(() =&gt; {
+</xsl:text>
+ <xsl:text> cb.dispatchEvent(eventUp);
+</xsl:text>
+ <xsl:text> }, 100);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> onMouseUp(evt) {
+</xsl:text>
+ <xsl:text> window.clearTimeout(this.touchTimer);
+</xsl:text>
+ <xsl:text> this.touchTimer = null;
+</xsl:text>
+ <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true);
+</xsl:text>
+ <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true);
+</xsl:text>
+ <xsl:text> const area = this.element.getBoundingClientRect();
+</xsl:text>
+ <xsl:text> var xDiff = (evt.pageX - this.startX) * 100.0 / area.width;
+</xsl:text>
+ <xsl:text> var yDiff = (evt.pageY - this.startY) * 100.0 / area.height;
+</xsl:text>
+ <xsl:text> var action = null;
+</xsl:text>
+ <xsl:text> if (xDiff &lt; 0 &amp;&amp; Math.abs(xDiff) &gt;= this.settings.left.xThreshold &amp;&amp; Math.abs(yDiff) &lt; this.settings.left.yThreshold) {
+</xsl:text>
+ <xsl:text> action = "left";
+</xsl:text>
+ <xsl:text> } else if (xDiff &gt; 0 &amp;&amp; Math.abs(xDiff) &gt;= this.settings.right.xThreshold &amp;&amp; Math.abs(yDiff) &lt; this.settings.right.yThreshold) {
+</xsl:text>
+ <xsl:text> action = "right";
+</xsl:text>
+ <xsl:text> } else if (yDiff &lt; 0 &amp;&amp; Math.abs(yDiff) &gt;= this.settings.up.yThreshold &amp;&amp; Math.abs(xDiff) &lt; this.settings.up.xThreshold) {
+</xsl:text>
+ <xsl:text> action = "up";
+</xsl:text>
+ <xsl:text> } else if (yDiff &gt; 0 &amp;&amp; Math.abs(yDiff) &gt;= this.settings.down.yThreshold &amp;&amp; Math.abs(xDiff) &lt; this.settings.down.xThreshold) {
+</xsl:text>
+ <xsl:text> action = "down";
+</xsl:text>
+ <xsl:text> } else if (Math.abs(xDiff) &lt; this.moveThreshold &amp;&amp; Math.abs(yDiff) &lt; this.moveThreshold) {
+</xsl:text>
+ <xsl:text> this.propagateMouseDownEvent(true);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> if (action) {
+</xsl:text>
+ <xsl:text> for (var a of this.settings[action].actions) {
+</xsl:text>
+ <xsl:text> if (a.action == "jump") {
+</xsl:text>
+ <xsl:text> fading_page_switch(a.target);
+</xsl:text>
+ <xsl:text> } else if (a.action == "change") {
+</xsl:text>
+ <xsl:text> this.change_hmi_value(a.var_idx, a.value);
+</xsl:text>
+ <xsl:text> } else if (a.action == "set") {
+</xsl:text>
+ <xsl:text> this.apply_hmi_value(a.var_idx, a.value);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> onMouseMove(evt) {
+</xsl:text>
+ <xsl:text> this.currX = evt.pageX;
+</xsl:text>
+ <xsl:text> this.currY = evt.pageY;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> onMouseDown(evt) {
+</xsl:text>
+ <xsl:text> this.startX = evt.pageX;
+</xsl:text>
+ <xsl:text> this.startY = evt.pageY;
+</xsl:text>
+ <xsl:text> this.currX = evt.pageX;
+</xsl:text>
+ <xsl:text> this.currY = evt.pageY;
+</xsl:text>
+ <xsl:text> svg_root.addEventListener("pointerup", this.boundOnMouseUp, true);
+</xsl:text>
+ <xsl:text> svg_root.addEventListener("pointermove", this.boundOnMouseMove, true);
+</xsl:text>
+ <xsl:text> this.touchTimer = window.setTimeout(() =&gt; {
+</xsl:text>
+ <xsl:text> const area = this.element.getBBox();
+</xsl:text>
+ <xsl:text> var xDiff = (this.currX - this.startX) * 100.0 / area.width;
+</xsl:text>
+ <xsl:text> var yDiff = (this.currY - this.startY) * 100.0 / area.height;
+</xsl:text>
+ <xsl:text> if (Math.abs(xDiff) &lt; this.moveThreshold &amp;&amp; Math.abs(yDiff) &lt; this.moveThreshold) {
+</xsl:text>
+ <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true);
+</xsl:text>
+ <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true);
+</xsl:text>
+ <xsl:text> this.propagateMouseDownEvent(false);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this.touchTimer = null;
+</xsl:text>
+ <xsl:text> }, this.pressTimeout);
+</xsl:text>
+ <xsl:text> this.request_animate();
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <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>
+ <xsl:with-param name="mandatory" select="'no'"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:value-of select="$disability"/>
+ <xsl:variable name="has_disability" select="string-length($disability)&gt;0"/>
+ <xsl:text> init: function() {
+</xsl:text>
+ <xsl:text> this.boundOnMouseUp = this.onMouseUp.bind(this);
+</xsl:text>
+ <xsl:text> this.boundOnMouseMove = this.onMouseMove.bind(this);
+</xsl:text>
+ <xsl:text> this.element.addEventListener("pointerdown", this.onMouseDown.bind(this));
+</xsl:text>
+ <xsl:text> const dirs = ["left", "right", "up", "down"];
+</xsl:text>
+ <xsl:text> var properDir = false;
+</xsl:text>
+ <xsl:text> var pathIndex = -1;
+</xsl:text>
+ <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:if>
+ </xsl:for-each>
+ </xsl:variable>
+ <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 &gt; 0) {
+</xsl:text>
+ <xsl:text> pathIndex = Number("</xsl:text>
+ <xsl:value-of select="$index"/>
+ <xsl:text>");
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> properDir = dirs.findIndex((x) =&gt; x == "</xsl:text>
+ <xsl:value-of select="$direction"/>
+ <xsl:text>") &gt; -1;
+</xsl:text>
+ <xsl:text> if (properDir) {
+</xsl:text>
+ <xsl:text> switch ("</xsl:text>
+ <xsl:value-of select="$command"/>
+ <xsl:text>") {
+</xsl:text>
+ <xsl:text> case "xthreshold":
+</xsl:text>
+ <xsl:text> this.settings["</xsl:text>
+ <xsl:value-of select="$direction"/>
+ <xsl:text>"].xThreshold = </xsl:text>
+ <xsl:value-of select="$value"/>
+ <xsl:text>;
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "ythreshold":
+</xsl:text>
+ <xsl:text> this.settings["</xsl:text>
+ <xsl:value-of select="$direction"/>
+ <xsl:text>"].yThreshold = </xsl:text>
+ <xsl:value-of select="$value"/>
+ <xsl:text>;
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "jump":
+</xsl:text>
+ <xsl:text> this.settings["</xsl:text>
+ <xsl:value-of select="$direction"/>
+ <xsl:text>"].actions.push({
+</xsl:text>
+ <xsl:text> action: "jump",
+</xsl:text>
+ <xsl:text> target: "</xsl:text>
+ <xsl:value-of select="$value"/>
+ <xsl:text>",
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "change":
+</xsl:text>
+ <xsl:text> this.settings["</xsl:text>
+ <xsl:value-of select="$direction"/>
+ <xsl:text>"].actions.push({
+</xsl:text>
+ <xsl:text> action: "change",
+</xsl:text>
+ <xsl:text> var_idx: pathIndex,
+</xsl:text>
+ <xsl:text> value: "</xsl:text>
+ <xsl:value-of select="$value"/>
+ <xsl:text>",
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> case "set":
+</xsl:text>
+ <xsl:text> this.settings["</xsl:text>
+ <xsl:value-of select="$direction"/>
+ <xsl:text>"].actions.push({
+</xsl:text>
+ <xsl:text> action: "set",
+</xsl:text>
+ <xsl:text> var_idx: pathIndex,
+</xsl:text>
+ <xsl:text> value: "</xsl:text>
+ <xsl:value-of select="$value"/>
+ <xsl:text>",
+</xsl:text>
+ <xsl:text> });
+</xsl:text>
+ <xsl:text> break;
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> if ("</xsl:text>
+ <xsl:value-of select="$name"/>
+ <xsl:text>" == "movethreshold") {
+</xsl:text>
+ <xsl:text> this.moveThreshold = "</xsl:text>
+ <xsl:value-of select="$value"/>
+ <xsl:text>";
+</xsl:text>
+ <xsl:text> } else if ("</xsl:text>
+ <xsl:value-of select="$name"/>
+ <xsl:text>" == "presstimeout") {
+</xsl:text>
+ <xsl:text> this.pressTimeout = "</xsl:text>
+ <xsl:value-of select="$value"/>
+ <xsl:text>";
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ </xsl:for-each>
+ <xsl:text> },
+</xsl:text>
+ </xsl:template>
+ <xsl:template match="widget[@type='TouchDetect']" mode="widget_desc">
+ <type>
+ <xsl:value-of select="@type"/>
+ </type>
+ <longdesc>
+ <xsl:text>TouchDetect publishes whether any finger is on the screen via one HMI variable (first path).
+</xsl:text>
+ <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>
+ <xsl:text>widget list). init() sets relativeness/offset so get_variable_index works without calling sub()
+</xsl:text>
+ <xsl:text>(sub() registers with subscribers and runs apply_cache; not needed for outbound-only touch writes).
+</xsl:text>
+ <xsl:text>Label example: HMI:TouchDetect@/YourBoolOrInt &#x2014; while at least one touch pointer is active the
+</xsl:text>
+ <xsl:text>variable is 1, otherwise 0. Listeners are on document.body (pointerdown/pointerup/pointercancel).
+</xsl:text>
+ </longdesc>
+ <shortdesc>
+ <xsl:text>Global touch active flag (body pointer events, touch pointers only)</xsl:text>
+ </shortdesc>
+ <path name="touch_active" count="1" accepts="HMI_BOOL,HMI_INT"/>
+ </xsl:template>
+ <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>
+ <xsl:text> init() {
+</xsl:text>
+ <xsl:text> const n = this.indexes ? this.indexes.length : 0;
+</xsl:text>
+ <xsl:text> if (n &gt; 0) {
+</xsl:text>
+ <xsl:text> this.offset = 0;
+</xsl:text>
+ <xsl:text> this.relativeness = Array(n).fill(false);
+</xsl:text>
+ <xsl:text> this.container_id = "";
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this._touchPointerIds = new Set();
+</xsl:text>
+ <xsl:text> this._onPointerDown = (e) =&gt; {
+</xsl:text>
+ <xsl:text> if (this._touchPointerIds.size === 0) {
+</xsl:text>
+ <xsl:text> this.apply_hmi_value(0, 1);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> this._touchPointerIds.add(e.pointerId);
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text> this._onPointerUp = (e) =&gt; {
+</xsl:text>
+ <xsl:text> this._touchPointerIds.delete(e.pointerId);
+</xsl:text>
+ <xsl:text> if (this._touchPointerIds.size === 0) {
+</xsl:text>
+ <xsl:text> this.apply_hmi_value(0, 0);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text> document.body.addEventListener("pointerdown", this._onPointerDown, { passive: true });
+</xsl:text>
+ <xsl:text> document.body.addEventListener("pointerup", this._onPointerUp, { passive: true });
+</xsl:text>
+ <xsl:text> document.body.addEventListener("pointercancel", this._onPointerUp, { passive: true });
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <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>
+ <xsl:with-param name="mandatory" select="'no'"/>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:value-of select="$disability"/>
+ <xsl:variable name="has_disability" select="string-length($disability)&gt;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:message>
+ </xsl:if>
+ </xsl:template>
+ <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>
+ <xsl:text> dispatch(value, oldval, varnum) {
+</xsl:text>
+ <xsl:text> if (varnum === 0) {
+</xsl:text>
+ <xsl:text> let dest_index = this.get_variable_index(1);
+</xsl:text>
+ <xsl:text> let current_dest_val = cache[dest_index];
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> if (value !== current_dest_val) {
+</xsl:text>
+ <xsl:text> this.apply_hmi_value(1, value);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> else if (varnum === 1) {
+</xsl:text>
+ <xsl:text> let src_index = this.get_variable_index(0);
+</xsl:text>
+ <xsl:text> if (value !== cache[src_index]) {
+</xsl:text>
+ <xsl:text> this.apply_hmi_value(0, value);
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> init() {
+</xsl:text>
+ <xsl:text> this.element.style.display = "none";
+</xsl:text>
+ <xsl:text> }
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
<xsl:template match="widget[@type='Assign']" mode="widget_desc">
<type>
<xsl:value-of select="@type"/>
@@ -9380,2140 +11616,6 @@
<xsl:text>
</xsl:text>
</xsl:template>
- <xsl:template match="widget[@type='AnimateRotation']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>AnimateRotation widget animates rotation of an SVG element. Widget is a group with label
-</xsl:text>
- <xsl:text>HMI:AnimateRotation:optional_args
-</xsl:text>
- <xsl:text>Element to rotate is a part of that group labeled "animate".
-</xsl:text>
- <xsl:text>Optional element of that group is a graphic whose label is one of: "center:top_left", "center:top_right",
-</xsl:text>
- <xsl:text>"center:bottom_left", "center:bottom_right" or "center:center". Label indicates which point of that element
-</xsl:text>
- <xsl:text>will be used as a center of rotation for "animate" element. If omitted, "animate" element's center will be used.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Optional arguments are:
-</xsl:text>
- <xsl:text>- duration=value: duration of a single loop in ms (if omitted, 2000 is set)
-</xsl:text>
- <xsl:text>- iterations=value: number of loops to be performed (if omitted, infinite number is set)
-</xsl:text>
- <xsl:text>- frame_rate=value: number of animation frames per second (if omitted, 10 will be used)
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>The higher the frame rate, the higher CPU usage will be.
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Rotation animation</xsl:text>
- </shortdesc>
- </xsl:template>
- <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>
- <xsl:text> duration = 2000;
-</xsl:text>
- <xsl:text> iterations = "infinite";
-</xsl:text>
- <xsl:text> center_x = null;
-</xsl:text>
- <xsl:text> center_y = null;
-</xsl:text>
- <xsl:text> frame_rate = 10;
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
- <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>
- <xsl:with-param name="mandatory" select="'no'"/>
- </xsl:call-template>
- </xsl:variable>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)&gt;0"/>
- <xsl:text> init() {
-</xsl:text>
- <xsl:variable name="widget_type" select="@type"/>
- <xsl:variable name="widget_id" select="@id"/>
- <xsl:text> const widget_pos = this.element.getBBox();
-</xsl:text>
- <xsl:text> this.center_x = widget_pos.x + widget_pos.width / 2;
-</xsl:text>
- <xsl:text> this.center_y = widget_pos.y + widget_pos.height / 2;
-</xsl:text>
- <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:text>;
-</xsl:text>
- </xsl:for-each>
- <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:text>");
-</xsl:text>
- <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>";
-</xsl:text>
- <xsl:text> const el_pos = el.getBBox();
-</xsl:text>
- <xsl:text> switch (el_label) {
-</xsl:text>
- <xsl:text> case "top_left":
-</xsl:text>
- <xsl:text> this.center_x = el_pos.x;
-</xsl:text>
- <xsl:text> this.center_y = el_pos.y;
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "top_right":
-</xsl:text>
- <xsl:text> this.center_x = el_pos.x + el_pos.width;
-</xsl:text>
- <xsl:text> this.center_y = el_pos.y;
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "bottom_left":
-</xsl:text>
- <xsl:text> this.center_x = el_pos.x;
-</xsl:text>
- <xsl:text> this.center_y = el_pos.y + el_pos.height;
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "bottom_right":
-</xsl:text>
- <xsl:text> this.center_x = el_pos.x + el_pos.width;
-</xsl:text>
- <xsl:text> this.center_y = el_pos.y + el_pos.height;
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "center":
-</xsl:text>
- <xsl:text> this.center_x = el_pos.x + el_pos.width / 2;
-</xsl:text>
- <xsl:text> this.center_y = el_pos.y + el_pos.height / 2;
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> this.element.removeChild(el);
-</xsl:text>
- </xsl:if>
- <xsl:if test="count($center_element) &gt; 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:variable>
- <xsl:message terminate="yes">
- <xsl:value-of select="$errmsg"/>
- </xsl:message>
- </xsl:if>
- <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:variable>
- <xsl:message terminate="yes">
- <xsl:value-of select="$errmsg"/>
- </xsl:message>
- </xsl:if>
- <xsl:text> var anim_el = id("</xsl:text>
- <xsl:value-of select="$animate_element/@id"/>
- <xsl:text>");
-</xsl:text>
- <xsl:text> anim_el.style.transformOrigin = vsprintf("%.2fpx %.2fpx", [this.center_x, this.center_y]);
-</xsl:text>
- <xsl:text> anim_el.style.animation = vsprintf("animateRotation %.3fs steps(%s) %s", [this.duration / 1000.0, this.frame_rate, this.iterations]);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- </xsl:template>
- <cssdefs:animaterotation/>
- <xsl:template match="cssdefs:animaterotation">
- <xsl:text>
-</xsl:text>
- <xsl:text>/* </xsl:text>
- <xsl:value-of select="local-name()"/>
- <xsl:text> */
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>@keyframes animateRotation {
-</xsl:text>
- <xsl:text> 100% { transform: rotate(360deg); }
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- <xsl:text>
-</xsl:text>
- </xsl:template>
- <xsl:template match="widget[@type='CloudImage']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>If CloudImage widget is a svg:image element, then href content is replaced by
-</xsl:text>
- <xsl:text>link to the file whose name is the value of given variable, being served from
-</xsl:text>
- <xsl:text>/media/data/cloud folder.
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Image display</xsl:text>
- </shortdesc>
- </xsl:template>
- <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>
- <xsl:text> frequency = 5;
-</xsl:text>
- <xsl:text> dispatch(value, oldval, index) {
-</xsl:text>
- <xsl:text> if (index == 0) {
-</xsl:text>
- <xsl:text> this.given_url = "cloudfolder?image=" + value;
-</xsl:text>
- <xsl:text> this.ready = true;
-</xsl:text>
- <xsl:text> this.request_animate();
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
- <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>
- <xsl:with-param name="mandatory" select="'no'"/>
- </xsl:call-template>
- </xsl:variable>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)&gt;0"/>
- <xsl:text> given_url: "",
-</xsl:text>
- <xsl:text> ready: false,
-</xsl:text>
- <xsl:text> animate: function(){
-</xsl:text>
- <xsl:text> this.element.setAttribute('href', this.given_url);
-</xsl:text>
- <xsl:text> },
-</xsl:text>
- </xsl:template>
- <xsl:template xmlns="http://www.w3.org/2000/svg" mode="inline_svg" match="svg:image[starts-with(@inkscape:label, 'HMI:CloudImage')]">
- <xsl:copy>
- <xsl:apply-templates mode="inline_svg" select="@*[not(contains(name(), 'href'))] | node()"/>
- </xsl:copy>
- </xsl:template>
- <xsl:template match="widget[@type='DropDownIndexed']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>DropDownIndexed widget can have one, two or three path variables.
-</xsl:text>
- <xsl:text>It needs "text" (svg:text or svg:use referring to svg:text),
-</xsl:text>
- <xsl:text>"box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect)
-</xsl:text>
- <xsl:text>labeled elements.
-</xsl:text>
- <xsl:text>When user clicks on "button", "text" is duplicated to display entries in the
-</xsl:text>
- <xsl:text>limit of available space in page, and "box" is extended to contain all
-</xsl:text>
- <xsl:text>texts.
-</xsl:text>
- <xsl:text>"highlight" is moved over pre-selected entry.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>The first variable path is index of selection, and the second is value of selection.
-</xsl:text>
- <xsl:text>In case there are one or two path variables, a list of texts is defined via
-</xsl:text>
- <xsl:text>arguments.
-</xsl:text>
- <xsl:text>If there are no arguments, it is expected that "text" labeled element is of
-</xsl:text>
- <xsl:text>type svg:use and refers to a svg:text element part of a TextList widget.
-</xsl:text>
- <xsl:text>In that case list of texts is set to TextList content.
-</xsl:text>
- <xsl:text>When only one argument is given and its value is "#langs" then list of
-</xsl:text>
- <xsl:text>texts is automatically set to the human-readable list of supported
-</xsl:text>
- <xsl:text>languages by this HMI.
-</xsl:text>
- <xsl:text>Otherwise, arguments are used as dropdown options.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>In case there are three path variables, the third path variable is a filter
-</xsl:text>
- <xsl:text>in a form of a string containing ':' separated list of indices of the options
-</xsl:text>
- <xsl:text>from the arguments that will be shown in the dropdown.
-</xsl:text>
- <xsl:text>Examples:
-</xsl:text>
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE
-</xsl:text>
- <xsl:text>HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Let user select text entry in a drop-down menu</xsl:text>
- </shortdesc>
- <arg name="entries" count="many" accepts="string">
- <xsl:text>drop-down menu entries</xsl:text>
- </arg>
- <path name="selected_index" accepts="HMI_INT">
- <xsl:text>selection index</xsl:text>
- </path>
- <path name="selected_value" accepts="HMI_STRING">
- <xsl:text>selection value</xsl:text>
- </path>
- <path name="filter" accepts="HMI_STRING">
- <xsl:text>indices of shown drop-down menu entries</xsl:text>
- </path>
- </xsl:template>
- <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>
- <xsl:text> dispatch(value, old_val, index) {
-</xsl:text>
- <xsl:text> if (index == 0) {
-</xsl:text>
- <xsl:text> if (!this.opened) this.set_selection(value);
-</xsl:text>
- <xsl:text> } else if (index == 2) {
-</xsl:text>
- <xsl:text> try {
-</xsl:text>
- <xsl:text> const desiredIndices = value.split(":").map((str) =&gt; +str);
-</xsl:text>
- <xsl:text> // Cache the original content to prevent data destruction on subsequent filters
-</xsl:text>
- <xsl:text> if (!this.original_content) {
-</xsl:text>
- <xsl:text> this.original_content = [...this.content];
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> this.content = this.original_content.filter((item, idx) =&gt; desiredIndices.includes(idx));
-</xsl:text>
- <xsl:text> } catch {}
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
- <declarations:DropDownIndexed/>
- <xsl:template match="declarations:DropDownIndexed">
- <xsl:text>
-</xsl:text>
- <xsl:text>/* </xsl:text>
- <xsl:value-of select="local-name()"/>
- <xsl:text> */
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> Object.getOwnPropertyNames(DropDownWidget.prototype).forEach(name =&gt; {
-</xsl:text>
- <xsl:text> if (name !== "constructor" &amp;&amp; name !== "dispatch") {
-</xsl:text>
- <xsl:text> DropDownIndexedWidget.prototype[name] = DropDownWidget.prototype[name];
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text>
-</xsl:text>
- </xsl:template>
- <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>
- <xsl:with-param name="mandatory" select="'no'"/>
- </xsl:call-template>
- </xsl:variable>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)&gt;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:with-param>
- </xsl:call-template>
- <xsl:variable name="text_elt" select="$hmi_element//*[@inkscape:label='text'][1]"/>
- <xsl:text>init_specific: function() {
-</xsl:text>
- <xsl:choose>
- <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>");
-</xsl:text>
- <xsl:text> this.content = langs.map(([lname,lcode]) =&gt; lname);
-</xsl:text>
- </xsl:when>
- <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:message>
- </xsl:if>
- <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:text>");
-</xsl:text>
- <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:message>
- </xsl:if>
- <xsl:text> this.content = hmi_widgets["</xsl:text>
- <xsl:value-of select="$from_list/@id"/>
- <xsl:text>"].texts;
-</xsl:text>
- </xsl:when>
- <xsl:otherwise>
- <xsl:text> this.text_elt = id("</xsl:text>
- <xsl:value-of select="$text_elt/@id"/>
- <xsl:text>");
-</xsl:text>
- <xsl:text> this.content = [
-</xsl:text>
- <xsl:for-each select="arg">
- <xsl:text> "</xsl:text>
- <xsl:value-of select="@value"/>
- <xsl:text>",
-</xsl:text>
- </xsl:for-each>
- <xsl:text> ];
-</xsl:text>
- </xsl:otherwise>
- </xsl:choose>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
- <xsl:template match="widget[@type='HistoryXYGraph']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>HistoryXYGraph draws a cartesian trend graph reusing styles given for axis,
-</xsl:text>
- <xsl:text>grid/marks, legends and curves.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Elements labeled "x_axis" and "y_axis" are svg:groups containing:
-</xsl:text>
- <xsl:text> - "axis_label" svg:text gives style an alignment for axis labels.
-</xsl:text>
- <xsl:text> - "interval_major_mark" and "interval_minor_mark" are svg elements to be
-</xsl:text>
- <xsl:text> duplicated along axis line to form intervals marks.
-</xsl:text>
- <xsl:text> - "axis_line" svg:path is the axis line. Paths must be intersect and their
-</xsl:text>
- <xsl:text> bounding box is the chart wall.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Elements labeled "curve_0", "curve_1", ... are paths whose styles are used
-</xsl:text>
- <xsl:text>to draw curves corresponding to data from variables passed as HMI tree paths.
-</xsl:text>
- <xsl:text>"curve_0" is mandatory. HMI variables outnumbering given curves are ignored.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Cartesian trend graph showing values of given variables over time</xsl:text>
- </shortdesc>
- <path name="value" count="1+" accepts="HMI_INT,HMI_REAL">
- <xsl:text>value</xsl:text>
- </path>
- <arg name="xformat" count="optional" accepts="string">
- <xsl:text>format string for X label</xsl:text>
- </arg>
- <arg name="yformat" count="optional" accepts="string">
- <xsl:text>format string for Y label</xsl:text>
- </arg>
- </xsl:template>
- <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>
- <xsl:text> frequency = 1;
-</xsl:text>
- <xsl:text> init() {
-</xsl:text>
- <xsl:text> this.params = [null, null];
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> [this.x_format, this.y_format] = this.args;
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this);
-</xsl:text>
- <xsl:text> this.loading = false;
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> this.curves = [];
-</xsl:text>
- <xsl:text> this.curves_data = [];
-</xsl:text>
- <xsl:text> this.init_specific();
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> this.reference = new ReferenceFrame(
-</xsl:text>
- <xsl:text> [[this.x_interval_minor_mark_elt, this.x_interval_major_mark_elt],
-</xsl:text>
- <xsl:text> [this.y_interval_minor_mark_elt, this.y_interval_major_mark_elt]],
-</xsl:text>
- <xsl:text> [this.x_axis_label_elt, this.y_axis_label_elt],
-</xsl:text>
- <xsl:text> [this.x_axis_line_elt, this.y_axis_line_elt],
-</xsl:text>
- <xsl:text> [this.x_format, this.y_format]);
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> let max_stroke_width = 0;
-</xsl:text>
- <xsl:text> for (let curve of this.curves) {
-</xsl:text>
- <xsl:text> if (curve.style.strokeWidth &gt; max_stroke_width) {
-</xsl:text>
- <xsl:text> max_stroke_width = curve.style.strokeWidth;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> this.curves_data.push([]);
-</xsl:text>
- <xsl:text> this.params.push(null);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> this.Margins = this.reference.getLengths().map(length =&gt; max_stroke_width / length);
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> // create &lt;clipPath&gt; path and attach it to widget
-</xsl:text>
- <xsl:text> let clipPath = document.createElementNS(xmlns, "clipPath");
-</xsl:text>
- <xsl:text> let clipPathPath = document.createElementNS(xmlns, "path");
-</xsl:text>
- <xsl:text> let clipPathPathDattr = document.createAttribute("d");
-</xsl:text>
- <xsl:text> clipPathPathDattr.value = this.reference.getClipPathPathDattr();
-</xsl:text>
- <xsl:text> clipPathPath.setAttributeNode(clipPathPathDattr);
-</xsl:text>
- <xsl:text> clipPath.appendChild(clipPathPath);
-</xsl:text>
- <xsl:text> clipPath.id = randomId();
-</xsl:text>
- <xsl:text> this.element.appendChild(clipPath);
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> // assign created clipPath to clip-path property of curves
-</xsl:text>
- <xsl:text> for(let curve of this.curves) {
-</xsl:text>
- <xsl:text> curve.setAttribute("clip-path", "url(#" + clipPath.id + ")");
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> fetch_error(e){
-</xsl:text>
- <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> do_http_request() {
-</xsl:text>
- <xsl:text> this.abort_controller = new AbortController();
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const decoder = new TextDecoder();
-</xsl:text>
- <xsl:text> let partialChunk = '';
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const query = {
-</xsl:text>
- <xsl:text> startTime: Date.parse(this.params[0]),
-</xsl:text>
- <xsl:text> endTime: Date.parse(this.params[1]),
-</xsl:text>
- <xsl:text> variableNames: this.params.slice(2)
-</xsl:text>
- <xsl:text> };
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const options = {
-</xsl:text>
- <xsl:text> method: 'POST',
-</xsl:text>
- <xsl:text> body: JSON.stringify(query),
-</xsl:text>
- <xsl:text> headers: { 'Content-Type': 'application/json' },
-</xsl:text>
- <xsl:text> signal: this.abort_controller.signal
-</xsl:text>
- <xsl:text> };
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> return fetch('/history', options)
-</xsl:text>
- <xsl:text> .then(response =&gt; {
-</xsl:text>
- <xsl:text> const reader = response.body.getReader();
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const read = () =&gt; {
-</xsl:text>
- <xsl:text> return reader.read().then(({ value, done }) =&gt; {
-</xsl:text>
- <xsl:text> if (done) return;
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const chunk = decoder.decode(value, { stream: true });
-</xsl:text>
- <xsl:text> const lines = (partialChunk + chunk).split(String.fromCharCode(10));
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> partialChunk = lines.pop();
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> lines.forEach(line =&gt; {
-</xsl:text>
- <xsl:text> if (line.trim()) {
-</xsl:text>
- <xsl:text> const row = JSON.parse(line);
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const vi = query.variableNames.findIndex(v =&gt; v === row.varname);
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> if (vi !== -1 &amp;&amp; this.curves_data[vi]) {
-</xsl:text>
- <xsl:text> this.curves_data[vi].push([row.timestamp, row.value]);
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> if (row.value &gt; this.ymax) this.ymax = row.value;
-</xsl:text>
- <xsl:text> if (row.value &lt; this.ymin) this.ymin = row.value;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> return read();
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> };
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> return read();
-</xsl:text>
- <xsl:text> }).catch(this.fetch_error_bound);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> unsub() {
-</xsl:text>
- <xsl:text> if (this.abort_controller) {
-</xsl:text>
- <xsl:text> this.abort_controller.abort();
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> super.unsub();
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> sub(...args){
-</xsl:text>
- <xsl:text> super.sub(...args);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> dispatch(value, oldval, index) {
-</xsl:text>
- <xsl:text> this.params[index] = value;
-</xsl:text>
- <xsl:text> if (this.params.every((item) =&gt; item !== null)) {
-</xsl:text>
- <xsl:text> if(!this.loading){
-</xsl:text>
- <xsl:text> this.loading = true;
-</xsl:text>
- <xsl:text> this.curves_data = [];
-</xsl:text>
- <xsl:text> for (let curve of this.curves) {
-</xsl:text>
- <xsl:text> this.curves_data.push([]);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> this.ymin = Infinity;
-</xsl:text>
- <xsl:text> this.ymax = -Infinity;
-</xsl:text>
- <xsl:text> this.do_http_request().finally(() =&gt; {
-</xsl:text>
- <xsl:text> let xmin = Infinity;
-</xsl:text>
- <xsl:text> let xmax = -Infinity;
-</xsl:text>
- <xsl:text> let has_data = false;
-</xsl:text>
- <xsl:text> for (let i = 0; i &lt; this.curves.length; i++) {
-</xsl:text>
- <xsl:text> const dataLength = this.curves_data[i].length;
-</xsl:text>
- <xsl:text> if (dataLength &gt; 1) {
-</xsl:text>
- <xsl:text> const ximin = this.curves_data[i][0][0];
-</xsl:text>
- <xsl:text> const ximax = this.curves_data[i][dataLength - 1][0];
-</xsl:text>
- <xsl:text> if (ximin &lt; xmin) xmin = ximin;
-</xsl:text>
- <xsl:text> if (ximax &gt; xmax) xmax = ximax;
-</xsl:text>
- <xsl:text> has_data = true;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> if (has_data) {
-</xsl:text>
- <xsl:text> this.xmin = xmin;
-</xsl:text>
- <xsl:text> this.xmax = xmax;
-</xsl:text>
- <xsl:text> } else {
-</xsl:text>
- <xsl:text> this.xmin = Date.parse(this.params[0]);
-</xsl:text>
- <xsl:text> this.xmax = Date.parse(this.params[1]);
-</xsl:text>
- <xsl:text> this.ymin = -1;
-</xsl:text>
- <xsl:text> this.ymax = 1;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> let Xrange = this.xmax - this.xmin;
-</xsl:text>
- <xsl:text> let Yrange = this.ymax - this.ymin;
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> // apply margin by moving min and max to enlarge range
-</xsl:text>
- <xsl:text> let [xMargin, yMargin] = zip(this.Margins, [Xrange, Yrange]).map(([m, l]) =&gt; m * l);
-</xsl:text>
- <xsl:text> [[this.dxmin, this.dxmax], [this.dymin, this.dymax]] =
-</xsl:text>
- <xsl:text> [[this.xmin - xMargin, this.xmax + xMargin],
-</xsl:text>
- <xsl:text> [this.ymin - yMargin, this.ymax + yMargin]];
-</xsl:text>
- <xsl:text> Xrange += 2 * xMargin;
-</xsl:text>
- <xsl:text> Yrange += 2 * yMargin;
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> // recompute curves "d" attribute
-</xsl:text>
- <xsl:text> let [base_point, xvect, yvect] = this.reference.getBaseRef();
-</xsl:text>
- <xsl:text> this.curves_d_attr =
-</xsl:text>
- <xsl:text> zip(this.curves_data, this.curves).map(([data, curve]) =&gt; {
-</xsl:text>
- <xsl:text> let new_d = data.map(([x, y], i) =&gt; {
-</xsl:text>
- <xsl:text> // compute curve point from data, ranges, and base_ref
-</xsl:text>
- <xsl:text> let xv = vectorscale(xvect, (x - this.dxmin) / Xrange);
-</xsl:text>
- <xsl:text> let yv = vectorscale(yvect, (y - this.dymin) / Yrange);
-</xsl:text>
- <xsl:text> let px = base_point.x + xv.x + yv.x;
-</xsl:text>
- <xsl:text> let py = base_point.y + xv.y + yv.y;
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> return " " + px + "," + py;
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> new_d.unshift("M ");
-</xsl:text>
- <xsl:text> return new_d.join('');
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> // computed curves "d" attr is applied to svg curve during animate();
-</xsl:text>
- <xsl:text> this.request_animate();
-</xsl:text>
- <xsl:text> this.loading = false;
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> animate() {
-</xsl:text>
- <xsl:text> this.reference.applyRanges([[this.dxmin, this.dxmax],
-</xsl:text>
- <xsl:text> [this.dymin, this.dymax]]);
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> // apply computed curves "d" attributes
-</xsl:text>
- <xsl:text> for (let [curve, d_attr] of zip(this.curves, this.curves_d_attr)) {
-</xsl:text>
- <xsl:text> if (d_attr.length &gt; 2)
-</xsl:text>
- <xsl:text> curve.setAttribute("d", d_attr);
-</xsl:text>
- <xsl:text> else
-</xsl:text>
- <xsl:text> curve.setAttribute("d", "M 0 0");
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
- <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>
- <xsl:with-param name="mandatory" select="'no'"/>
- </xsl:call-template>
- </xsl:variable>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)&gt;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:with-param>
- </xsl:call-template>
- <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:with-param>
- </xsl:call-template>
- <xsl:text> init_specific() {
-</xsl:text>
- <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:message>
- </xsl:if>
- <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:text> */
-</xsl:text>
- </xsl:for-each>
- <xsl:text> }
-</xsl:text>
- </xsl:template>
- <xsl:template match="widget[@type='MultiLangJsonTable']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>Send given variables as POST to http URL argument, spread returned JSON in
-</xsl:text>
- <xsl:text>SVG sub-elements of "data" labeled element.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Documentation to be written. see svghmi example.
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Http POST variables, spread JSON back</xsl:text>
- </shortdesc>
- <arg name="url" accepts="string"/>
- <path name="edit" accepts="HMI_INT, HMI_REAL, HMI_STRING">
- <xsl:text>single variable to edit</xsl:text>
- </path>
- </xsl:template>
- <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>
- <xsl:text> // arbitrary defaults to avoid missing entries in query
-</xsl:text>
- <xsl:text> cache = [0,0,0];
-</xsl:text>
- <xsl:text> init_common() {
-</xsl:text>
- <xsl:text> this.spread_json_data_bound = this.spread_json_data.bind(this);
-</xsl:text>
- <xsl:text> this.handle_http_response_bound = this.handle_http_response.bind(this);
-</xsl:text>
- <xsl:text> this.fetch_error_bound = this.fetch_error.bind(this);
-</xsl:text>
- <xsl:text> if (this.should_translate === undefined) {
-</xsl:text>
- <xsl:text> this.should_translate = [];
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> if (this.lang_keys === undefined) {
-</xsl:text>
- <xsl:text> this.lang_keys = [];
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> this.promised = false;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> handle_http_response(response) {
-</xsl:text>
- <xsl:text> if (!response.ok) {
-</xsl:text>
- <xsl:text> console.log("HTTP error, status = " + response.status);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> return response.json();
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> fetch_error(e){
-</xsl:text>
- <xsl:text> console.log("HTTP fetch error, message = " + e.message + "Widget:" + this.element_id);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> do_http_request(...opt) {
-</xsl:text>
- <xsl:text> this.abort_controller = new AbortController();
-</xsl:text>
- <xsl:text> return Promise.resolve().then(() =&gt; {
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const query = {
-</xsl:text>
- <xsl:text> args: this.args,
-</xsl:text>
- <xsl:text> range: this.cache[1],
-</xsl:text>
- <xsl:text> position: this.cache[2],
-</xsl:text>
- <xsl:text> visible: this.visible,
-</xsl:text>
- <xsl:text> extra: this.cache.slice(4),
-</xsl:text>
- <xsl:text> options: opt
-</xsl:text>
- <xsl:text> };
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> const options = {
-</xsl:text>
- <xsl:text> method: 'POST',
-</xsl:text>
- <xsl:text> body: JSON.stringify(query),
-</xsl:text>
- <xsl:text> headers: {'Content-Type': 'application/json'},
-</xsl:text>
- <xsl:text> signal: this.abort_controller.signal
-</xsl:text>
- <xsl:text> };
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> return fetch(this.args[0], options)
-</xsl:text>
- <xsl:text> .then(this.handle_http_response_bound)
-</xsl:text>
- <xsl:text> .then(this.spread_json_data_bound)
-</xsl:text>
- <xsl:text> .catch(this.fetch_error_bound);
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> unsub(){
-</xsl:text>
- <xsl:text> this.abort_controller.abort();
-</xsl:text>
- <xsl:text> super.unsub();
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> sub(...args){
-</xsl:text>
- <xsl:text> this.cache[0] = undefined;
-</xsl:text>
- <xsl:text> super.sub(...args);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> dispatch(value, oldval, index) {
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> if(this.cache[index] != value)
-</xsl:text>
- <xsl:text> this.cache[index] = value;
-</xsl:text>
- <xsl:text> else
-</xsl:text>
- <xsl:text> return;
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> if(!this.promised){
-</xsl:text>
- <xsl:text> this.promised = true;
-</xsl:text>
- <xsl:text> this.do_http_request().finally(() =&gt; {
-</xsl:text>
- <xsl:text> this.promised = false;
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> make_on_click(...options){
-</xsl:text>
- <xsl:text> let that = this;
-</xsl:text>
- <xsl:text> return function(evt){
-</xsl:text>
- <xsl:text> that.do_http_request(...options);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> // on_click(evt, ...options) {
-</xsl:text>
- <xsl:text> // this.do_http_request(...options);
-</xsl:text>
- <xsl:text> // }
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
- <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()"/>
- <xsl:text>.</xsl:text>
- </xsl:message>
- </xsl:template>
- <func:function name="func:ml_json_expressions">
- <xsl:param name="expressions"/>
- <xsl:param name="label"/>
- <xsl:choose>
- <xsl:when test="$label">
- <xsl:variable name="suffixes" select="str:split($label)"/>
- <xsl:variable name="res">
- <xsl:for-each select="$suffixes">
- <expression>
- <xsl:variable name="suffix" select="."/>
- <xsl:variable name="pos" select="position()"/>
- <xsl:variable name="expr" select="$expressions[position() &lt;= $pos][last()]/expression"/>
- <xsl:if test="$pos = 1">
- <xsl:variable name="raw_selector" select="$suffix"/>
- <xsl:variable name="lang_selector">
- <xsl:choose>
- <xsl:when test="starts-with($raw_selector, '.')">
- <xsl:value-of select="substring($raw_selector, 2)"/>
- </xsl:when>
- <xsl:otherwise>
- <xsl:value-of select="$raw_selector"/>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:variable>
- <xsl:attribute name="lang_selector">
- <xsl:value-of select="$lang_selector"/>
- </xsl:attribute>
- </xsl:if>
- <xsl:choose>
- <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:message>
- </xsl:if>
- <xsl:attribute name="name">
- <xsl:value-of select="$name"/>
- </xsl:attribute>
- <xsl:choose>
- <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:choose>
- <xsl:when test="starts-with($raw_key, '.')">
- <xsl:value-of select="substring($raw_key, 2)"/>
- </xsl:when>
- <xsl:otherwise>
- <xsl:value-of select="$raw_key"/>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:variable>
- <xsl:attribute name="translation_key">
- <xsl:value-of select="$clean_key"/>
- </xsl:attribute>
- <xsl:attribute name="content">
- <xsl:value-of select="$expr/@content"/>
- <xsl:value-of select="$raw_key"/>
- </xsl:attribute>
- </xsl:when>
- <xsl:otherwise>
- <xsl:attribute name="content">
- <xsl:value-of select="$expr/@content"/>
- <xsl:value-of select="$content_raw"/>
- </xsl:attribute>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:when>
- <xsl:otherwise>
- <xsl:copy-of select="$expr/@name"/>
- <xsl:attribute name="content">
- <xsl:value-of select="$expr/@content"/>
- <xsl:value-of select="$suffix"/>
- </xsl:attribute>
- </xsl:otherwise>
- </xsl:choose>
- </expression>
- </xsl:for-each>
- </xsl:variable>
- <func:result select="exsl:node-set($res)"/>
- </xsl:when>
- <xsl:otherwise>
- <func:result select="$expressions"/>
- </xsl:otherwise>
- </xsl:choose>
- </func:function>
- <xsl:variable name="ml_initexpr">
- <expression>
- <xsl:attribute name="content">
- <xsl:text>jdata</xsl:text>
- </xsl:attribute>
- </expression>
- </xsl:variable>
- <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:choose>
- <xsl:when test="count($from_list) &gt; 0">
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").href.baseVal =
-</xsl:text>
- <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:text>];
-</xsl:text>
- </xsl:when>
- <xsl:otherwise>
- <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:message>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:template>
- <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:choose>
- <xsl:when test="count($from_textstylelist) &gt; 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:message>
- </xsl:if>
- <xsl:text> {
-</xsl:text>
- <xsl:text> let elt = id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>");
-</xsl:text>
- <xsl:text> elt.textContent = String(</xsl:text>
- <xsl:value-of select="$content_expr"/>
- <xsl:text>);
-</xsl:text>
- <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>];
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- </xsl:when>
- <xsl:otherwise>
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").textContent = String(</xsl:text>
- <xsl:value-of select="$value_expr"/>
- <xsl:text>);
-</xsl:text>
- </xsl:otherwise>
- </xsl:choose>
- </xsl:template>
- <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:text>));
-</xsl:text>
- </xsl:template>
- <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:apply-templates>
- </xsl:if>
- </xsl:template>
- <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() &gt; 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:text>);
-</xsl:text>
- </xsl:for-each>
- <xsl:apply-templates mode="json_table_elt_render" select=".">
- <xsl:with-param name="expressions" select="$new_expressions"/>
- </xsl:apply-templates>
- </xsl:template>
- <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:text>_</xsl:text>
- </xsl:variable>
- <xsl:text> try {
-</xsl:text>
- <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>;
-</xsl:text>
- <xsl:text> if(</xsl:text>
- <xsl:value-of select="$varprefix"/>
- <xsl:value-of select="position()"/>
- <xsl:text> == undefined) {
-</xsl:text>
- <xsl:text> throw null;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- </xsl:for-each>
- <xsl:variable name="new_expressions">
- <xsl:for-each select="$expressions/expression">
- <xsl:copy>
- <xsl:copy-of select="@name"/>
- <xsl:attribute name="content">
- <xsl:value-of select="$varprefix"/>
- <xsl:value-of select="position()"/>
- </xsl:attribute>
- </xsl:copy>
- </xsl:for-each>
- </xsl:variable>
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").style = "</xsl:text>
- <xsl:value-of select="@style"/>
- <xsl:text>";
-</xsl:text>
- <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:apply-templates>
- <xsl:text> } catch(err) {
-</xsl:text>
- <xsl:text> id("</xsl:text>
- <xsl:value-of select="@id"/>
- <xsl:text>").style = "display:none";
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- </xsl:template>
- <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>
- <xsl:with-param name="mandatory" select="'no'"/>
- </xsl:call-template>
- </xsl:variable>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)&gt;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:with-param>
- </xsl:call-template>
- <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:for-each>
- </xsl:variable>
- <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>,
-</xsl:text>
- <xsl:text> should_translate: [
-</xsl:text>
- <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:text>"
-</xsl:text>
- <xsl:if test="position() != last()">
- <xsl:text>,
-</xsl:text>
- </xsl:if>
- </xsl:if>
- </xsl:for-each>
- <xsl:text> ],
-</xsl:text>
- <xsl:text> lang_keys: [
-</xsl:text>
- <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:text>"
-</xsl:text>
- <xsl:if test="position() != last()">
- <xsl:text>,
-</xsl:text>
- </xsl:if>
- </xsl:if>
- </xsl:for-each>
- <xsl:text> ],
-</xsl:text>
- <xsl:text> spread_json_data: function(janswer) {
-</xsl:text>
- <xsl:text> let [range,position,jdata] = janswer;
-</xsl:text>
- <xsl:text> if (jdata.length &gt; 0 &amp;&amp; this.should_translate.length &gt; 0) {
-</xsl:text>
- <xsl:text> const lang = cache[lang_local_index];
-</xsl:text>
- <xsl:text> const langcode = langs[lang][1];
-</xsl:text>
- <xsl:text> for (let row of jdata) {
-</xsl:text>
- <xsl:text> for (const key of this.should_translate) {
-</xsl:text>
- <xsl:text> if (key in row) {
-</xsl:text>
- <xsl:text> const orig = row[key];
-</xsl:text>
- <xsl:text> const match = translations.find(item =&gt; item[1][0] == orig);
-</xsl:text>
- <xsl:text> const tr = match ? match[1][lang] : orig;
-</xsl:text>
- <xsl:text> row[key] = tr;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> for (const key of this.lang_keys) {
-</xsl:text>
- <xsl:text> if (key in row) {
-</xsl:text>
- <xsl:text> row[key] = String(row[key]) + "_" + langcode;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> [[1, range], [2, position], [3, this.visible]].map(([i,v]) =&gt; {
-</xsl:text>
- <xsl:text> this.apply_hmi_value(i,v);
-</xsl:text>
- <xsl:text> this.cache[i] = v;
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <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:apply-templates>
- <xsl:text> },
-</xsl:text>
- <xsl:text> init() {
-</xsl:text>
- <xsl:text> this.init_common();
-</xsl:text>
- <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:text>");
-</xsl:text>
- </xsl:for-each>
- <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:text>
- </xsl:for-each>
- <xsl:text> }
-</xsl:text>
- </xsl:template>
- <xsl:template match="widget[@type='Swipe']" mode="widget_desc">
- <type>
- <xsl:value-of select="@type"/>
- </type>
- <longdesc>
- <xsl:text>Swipe widget detects left, right, up and down swiping motion and executes
-</xsl:text>
- <xsl:text>associated actions. The widget should be placed on top of the area where the
-</xsl:text>
- <xsl:text>movement should be detected. It is a group containing a graphical element
-</xsl:text>
- <xsl:text>"area" which defines the area where the swipe should be detected.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>For each of the motions to be detected there must exist several parameters
-</xsl:text>
- <xsl:text>named "{direction}_{command}={value}" where {direction} is from the set:
-</xsl:text>
- <xsl:text>left, right, up, down; and {command} is from the set: xthreshold (in percents
-</xsl:text>
- <xsl:text>of widget width), ythreshold (also percentage), jump (value should be name of
-</xsl:text>
- <xsl:text>the page to jump to), change (value should be the change to apply, e.g. +2 to
-</xsl:text>
- <xsl:text>increase by 2, or -1 to decrement) or set (value should be the value to set
-</xsl:text>
- <xsl:text>to). change and set commands should also be accompanied by paths with the
-</xsl:text>
- <xsl:text>same name and their values should be variable names to apply the command to.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Additional parameters to add are:
-</xsl:text>
- <xsl:text> - movethreshold: Percentage of the widget dimensions that define the pointer
-</xsl:text>
- <xsl:text> movement. Anything below that value will not be considered
-</xsl:text>
- <xsl:text> a movement. If omitted, 5 will be used.
-</xsl:text>
- <xsl:text> - presstimeout: Time in milliseconds which will be measured on the pointer
-</xsl:text>
- <xsl:text> down event. If time elapses without any significant movement
-</xsl:text>
- <xsl:text> (defined by movethreshold), the pointer down/click event
-</xsl:text>
- <xsl:text> will be propagated on an element on a lower level than the
-</xsl:text>
- <xsl:text> swiping area. Similar thing will happen on pointer up event
-</xsl:text>
- <xsl:text> if there was no significant movement.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text>Examples:
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <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>
- <xsl:text>
-</xsl:text>
- <xsl:text>This detects left and right swipe motion. To detect swipe left, movement must
-</xsl:text>
- <xsl:text>be at least 30% of the widget width to the left and at most 5% of the widget
-</xsl:text>
- <xsl:text>height up or down. If detected, it will jump to a page named Home. To detect
-</xsl:text>
- <xsl:text>swipe up, movement must be at most 5% of the widget width left or right, and
-</xsl:text>
- <xsl:text>at least 25% of the widget height up. If detected, it will increase VAR0 by 2.
-</xsl:text>
- <xsl:text>
-</xsl:text>
- </longdesc>
- <shortdesc>
- <xsl:text>Detect swipe motion and react accordingly</xsl:text>
- </shortdesc>
- </xsl:template>
- <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>
- <xsl:text> frequency = 2;
-</xsl:text>
- <xsl:text> startX = -1;
-</xsl:text>
- <xsl:text> startY = -1;
-</xsl:text>
- <xsl:text> currX = -1;
-</xsl:text>
- <xsl:text> currY = -1;
-</xsl:text>
- <xsl:text> moveThreshold = 5;
-</xsl:text>
- <xsl:text> pressTimeout = 300;
-</xsl:text>
- <xsl:text> touchTimer = null;
-</xsl:text>
- <xsl:text> settings = {
-</xsl:text>
- <xsl:text> left: {
-</xsl:text>
- <xsl:text> actions: [],
-</xsl:text>
- <xsl:text> xThreshold: 100,
-</xsl:text>
- <xsl:text> yThreshold: 0,
-</xsl:text>
- <xsl:text> },
-</xsl:text>
- <xsl:text> right: {
-</xsl:text>
- <xsl:text> actions: [],
-</xsl:text>
- <xsl:text> xThreshold: 100,
-</xsl:text>
- <xsl:text> yThreshold: 0,
-</xsl:text>
- <xsl:text> },
-</xsl:text>
- <xsl:text> up: {
-</xsl:text>
- <xsl:text> actions: [],
-</xsl:text>
- <xsl:text> xThreshold: 0,
-</xsl:text>
- <xsl:text> yThreshold: 100,
-</xsl:text>
- <xsl:text> },
-</xsl:text>
- <xsl:text> down: {
-</xsl:text>
- <xsl:text> actions: [],
-</xsl:text>
- <xsl:text> xThreshold: 0,
-</xsl:text>
- <xsl:text> yThreshold: 100,
-</xsl:text>
- <xsl:text> },
-</xsl:text>
- <xsl:text> };
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> propagateMouseDownEvent(simulateUp) {
-</xsl:text>
- <xsl:text> const elements = document.elementsFromPoint(this.startX, this.startY);
-</xsl:text>
- <xsl:text> if (elements.length &gt; 1) {
-</xsl:text>
- <xsl:text> const eventDown = new MouseEvent("pointerdown", {
-</xsl:text>
- <xsl:text> view: window,
-</xsl:text>
- <xsl:text> bubbles: true,
-</xsl:text>
- <xsl:text> cancelable: true,
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> const eventClick = new MouseEvent("click", {
-</xsl:text>
- <xsl:text> view: window,
-</xsl:text>
- <xsl:text> bubbles: true,
-</xsl:text>
- <xsl:text> cancelable: true,
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> const eventUp = new MouseEvent("pointerup", {
-</xsl:text>
- <xsl:text> view: window,
-</xsl:text>
- <xsl:text> bubbles: true,
-</xsl:text>
- <xsl:text> cancelable: true,
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> const cb = document.getElementById(elements[1].id);
-</xsl:text>
- <xsl:text> cb.dispatchEvent(eventDown);
-</xsl:text>
- <xsl:text> cb.dispatchEvent(eventClick);
-</xsl:text>
- <xsl:text> if (simulateUp) {
-</xsl:text>
- <xsl:text> window.setTimeout(() =&gt; {
-</xsl:text>
- <xsl:text> cb.dispatchEvent(eventUp);
-</xsl:text>
- <xsl:text> }, 100);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> onMouseUp(evt) {
-</xsl:text>
- <xsl:text> window.clearTimeout(this.touchTimer);
-</xsl:text>
- <xsl:text> this.touchTimer = null;
-</xsl:text>
- <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true);
-</xsl:text>
- <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true);
-</xsl:text>
- <xsl:text> const area = this.element.getBoundingClientRect();
-</xsl:text>
- <xsl:text> var xDiff = (evt.pageX - this.startX) * 100.0 / area.width;
-</xsl:text>
- <xsl:text> var yDiff = (evt.pageY - this.startY) * 100.0 / area.height;
-</xsl:text>
- <xsl:text> var action = null;
-</xsl:text>
- <xsl:text> if (xDiff &lt; 0 &amp;&amp; Math.abs(xDiff) &gt;= this.settings.left.xThreshold &amp;&amp; Math.abs(yDiff) &lt; this.settings.left.yThreshold) {
-</xsl:text>
- <xsl:text> action = "left";
-</xsl:text>
- <xsl:text> } else if (xDiff &gt; 0 &amp;&amp; Math.abs(xDiff) &gt;= this.settings.right.xThreshold &amp;&amp; Math.abs(yDiff) &lt; this.settings.right.yThreshold) {
-</xsl:text>
- <xsl:text> action = "right";
-</xsl:text>
- <xsl:text> } else if (yDiff &lt; 0 &amp;&amp; Math.abs(yDiff) &gt;= this.settings.up.yThreshold &amp;&amp; Math.abs(xDiff) &lt; this.settings.up.xThreshold) {
-</xsl:text>
- <xsl:text> action = "up";
-</xsl:text>
- <xsl:text> } else if (yDiff &gt; 0 &amp;&amp; Math.abs(yDiff) &gt;= this.settings.down.yThreshold &amp;&amp; Math.abs(xDiff) &lt; this.settings.down.xThreshold) {
-</xsl:text>
- <xsl:text> action = "down";
-</xsl:text>
- <xsl:text> } else if (Math.abs(xDiff) &lt; this.moveThreshold &amp;&amp; Math.abs(yDiff) &lt; this.moveThreshold) {
-</xsl:text>
- <xsl:text> this.propagateMouseDownEvent(true);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> if (action) {
-</xsl:text>
- <xsl:text> for (var a of this.settings[action].actions) {
-</xsl:text>
- <xsl:text> if (a.action == "jump") {
-</xsl:text>
- <xsl:text> fading_page_switch(a.target);
-</xsl:text>
- <xsl:text> } else if (a.action == "change") {
-</xsl:text>
- <xsl:text> this.change_hmi_value(a.var_idx, a.value);
-</xsl:text>
- <xsl:text> } else if (a.action == "set") {
-</xsl:text>
- <xsl:text> this.apply_hmi_value(a.var_idx, a.value);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> onMouseMove(evt) {
-</xsl:text>
- <xsl:text> this.currX = evt.pageX;
-</xsl:text>
- <xsl:text> this.currY = evt.pageY;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> onMouseDown(evt) {
-</xsl:text>
- <xsl:text> this.startX = evt.pageX;
-</xsl:text>
- <xsl:text> this.startY = evt.pageY;
-</xsl:text>
- <xsl:text> this.currX = evt.pageX;
-</xsl:text>
- <xsl:text> this.currY = evt.pageY;
-</xsl:text>
- <xsl:text> svg_root.addEventListener("pointerup", this.boundOnMouseUp, true);
-</xsl:text>
- <xsl:text> svg_root.addEventListener("pointermove", this.boundOnMouseMove, true);
-</xsl:text>
- <xsl:text> this.touchTimer = window.setTimeout(() =&gt; {
-</xsl:text>
- <xsl:text> const area = this.element.getBBox();
-</xsl:text>
- <xsl:text> var xDiff = (this.currX - this.startX) * 100.0 / area.width;
-</xsl:text>
- <xsl:text> var yDiff = (this.currY - this.startY) * 100.0 / area.height;
-</xsl:text>
- <xsl:text> if (Math.abs(xDiff) &lt; this.moveThreshold &amp;&amp; Math.abs(yDiff) &lt; this.moveThreshold) {
-</xsl:text>
- <xsl:text> svg_root.removeEventListener("pointerup", this.boundOnMouseUp, true);
-</xsl:text>
- <xsl:text> svg_root.removeEventListener("pointermove", this.boundOnMouseMove, true);
-</xsl:text>
- <xsl:text> this.propagateMouseDownEvent(false);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> this.touchTimer = null;
-</xsl:text>
- <xsl:text> }, this.pressTimeout);
-</xsl:text>
- <xsl:text> this.request_animate();
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
- <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>
- <xsl:with-param name="mandatory" select="'no'"/>
- </xsl:call-template>
- </xsl:variable>
- <xsl:value-of select="$disability"/>
- <xsl:variable name="has_disability" select="string-length($disability)&gt;0"/>
- <xsl:text> init: function() {
-</xsl:text>
- <xsl:text> this.boundOnMouseUp = this.onMouseUp.bind(this);
-</xsl:text>
- <xsl:text> this.boundOnMouseMove = this.onMouseMove.bind(this);
-</xsl:text>
- <xsl:text> this.element.addEventListener("pointerdown", this.onMouseDown.bind(this));
-</xsl:text>
- <xsl:text> const dirs = ["left", "right", "up", "down"];
-</xsl:text>
- <xsl:text> var properDir = false;
-</xsl:text>
- <xsl:text> var pathIndex = -1;
-</xsl:text>
- <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:if>
- </xsl:for-each>
- </xsl:variable>
- <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 &gt; 0) {
-</xsl:text>
- <xsl:text> pathIndex = Number("</xsl:text>
- <xsl:value-of select="$index"/>
- <xsl:text>");
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> properDir = dirs.findIndex((x) =&gt; x == "</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>") &gt; -1;
-</xsl:text>
- <xsl:text> if (properDir) {
-</xsl:text>
- <xsl:text> switch ("</xsl:text>
- <xsl:value-of select="$command"/>
- <xsl:text>") {
-</xsl:text>
- <xsl:text> case "xthreshold":
-</xsl:text>
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].xThreshold = </xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text>;
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "ythreshold":
-</xsl:text>
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].yThreshold = </xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text>;
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "jump":
-</xsl:text>
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].actions.push({
-</xsl:text>
- <xsl:text> action: "jump",
-</xsl:text>
- <xsl:text> target: "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text>",
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "change":
-</xsl:text>
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].actions.push({
-</xsl:text>
- <xsl:text> action: "change",
-</xsl:text>
- <xsl:text> var_idx: pathIndex,
-</xsl:text>
- <xsl:text> value: "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text>",
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> case "set":
-</xsl:text>
- <xsl:text> this.settings["</xsl:text>
- <xsl:value-of select="$direction"/>
- <xsl:text>"].actions.push({
-</xsl:text>
- <xsl:text> action: "set",
-</xsl:text>
- <xsl:text> var_idx: pathIndex,
-</xsl:text>
- <xsl:text> value: "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text>",
-</xsl:text>
- <xsl:text> });
-</xsl:text>
- <xsl:text> break;
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> if ("</xsl:text>
- <xsl:value-of select="$name"/>
- <xsl:text>" == "movethreshold") {
-</xsl:text>
- <xsl:text> this.moveThreshold = "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text>";
-</xsl:text>
- <xsl:text> } else if ("</xsl:text>
- <xsl:value-of select="$name"/>
- <xsl:text>" == "presstimeout") {
-</xsl:text>
- <xsl:text> this.pressTimeout = "</xsl:text>
- <xsl:value-of select="$value"/>
- <xsl:text>";
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- </xsl:for-each>
- <xsl:text> },
-</xsl:text>
- </xsl:template>
- <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>
- <xsl:text> dispatch(value, oldval, varnum) {
-</xsl:text>
- <xsl:text> if (varnum === 0) {
-</xsl:text>
- <xsl:text> let dest_index = this.get_variable_index(1);
-</xsl:text>
- <xsl:text> let current_dest_val = cache[dest_index];
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> if (value !== current_dest_val) {
-</xsl:text>
- <xsl:text> this.apply_hmi_value(1, value);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> else if (varnum === 1) {
-</xsl:text>
- <xsl:text> let src_index = this.get_variable_index(0);
-</xsl:text>
- <xsl:text> if (value !== cache[src_index]) {
-</xsl:text>
- <xsl:text> this.apply_hmi_value(0, value);
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>
-</xsl:text>
- <xsl:text> init() {
-</xsl:text>
- <xsl:text> this.element.style.display = "none";
-</xsl:text>
- <xsl:text> }
-</xsl:text>
- <xsl:text>}
-</xsl:text>
- </xsl:template>
<xsl:template match="/">
<xsl:comment>
<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") {
+ longdesc
+ ||
+ 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") {
+||
+ init() {
+ const n = this.indexes ? this.indexes.length : 0;
+ if (n > 0) {
+ this.offset = 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") {
+ if "count(path) != 1"
+ error > TouchDetect id="«@id»": exactly one HMI path is required (e.g. HMI:TouchDetect@/PLC_TOUCH_ACTIVE)
+}