lpcmanager

Parents 27368b88c253
Children 34827d0071d3
New widget HMI:DropDownIndexed was created to accommodate runtime changes of dropdown options, as well as mitigate string length limitations. Redmine issue #4530
--- a/LPCSVGHMI/analyse_widget.xslt Fri Jan 30 08:41:19 2026 +0100
+++ b/LPCSVGHMI/analyse_widget.xslt Tue Mar 31 11:06:21 2026 +0200
@@ -1162,6 +1162,80 @@
<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"/>
--- a/LPCSVGHMI/gen_index_xhtml.xslt Fri Jan 30 08:41:19 2026 +0100
+++ b/LPCSVGHMI/gen_index_xhtml.xslt Tue Mar 31 11:06:21 2026 +0200
@@ -9649,6 +9649,217 @@
<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"/>
@@ -11255,6 +11466,54 @@
<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_dropdownindexed.ysl2 Tue Mar 31 11:06:21 2026 +0200
@@ -0,0 +1,104 @@
+// widget_dropdownindexed.ysl2
+
+widget_desc("DropDownIndexed") {
+
+ longdesc
+ ||
+ DropDownIndexed widget can have one, two or three path variables.
+ It needs "text" (svg:text or svg:use referring to svg:text),
+ "box" (svg:rect), "button" (svg:*), and "highlight" (svg:rect)
+ labeled elements.
+ When user clicks on "button", "text" is duplicated to display entries in the
+ limit of available space in page, and "box" is extended to contain all
+ texts.
+ "highlight" is moved over pre-selected entry.
+
+ The first variable path is index of selection, and the second is value of selection.
+ In case there are one or two path variables, a list of texts is defined via
+ arguments.
+ If there are no arguments, it is expected that "text" labeled element is of
+ type svg:use and refers to a svg:text element part of a TextList widget.
+ In that case list of texts is set to TextList content.
+ When only one argument is given and its value is "#langs" then list of
+ texts is automatically set to the human-readable list of supported
+ languages by this HMI.
+ Otherwise, arguments are used as dropdown options.
+
+ In case there are three path variables, the third path variable is a filter
+ in a form of a string containing ':' separated list of indices of the options
+ from the arguments that will be shown in the dropdown.
+ Examples:
+ HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE
+ HMI:DropDownIndexed:Red:Green:Blue:Other@/SELECTED_INDEX@/SELECTED_VALUE@/FILTER
+ ||
+ shortdesc > Let user select text entry in a drop-down menu
+
+ arg name="entries" count="many" accepts="string" > drop-down menu entries
+
+ path name="selected_index" accepts="HMI_INT" > selection index
+ path name="selected_value" accepts="HMI_STRING" > selection value
+ path name="filter" accepts="HMI_STRING" > indices of shown drop-down menu entries
+}
+
+// TODO: support i18n of menu entries using svg:text elements with labels starting with "_"
+
+widget_class("DropDownIndexed") {
+||
+ dispatch(value, old_val, index) {
+ if (index == 0) {
+ if (!this.opened) this.set_selection(value);
+ } else if (index == 2) {
+ try {
+ const desiredIndices = value.split(":").map((str) => +str);
+ // Cache the original content to prevent data destruction on subsequent filters
+ if (!this.original_content) {
+ this.original_content = [...this.content];
+ }
+ this.content = this.original_content.filter((item, idx) => desiredIndices.includes(idx));
+ } catch {}
+ }
+ }
+||
+}
+
+// Inherit all other methods natively from DropDownWidget to avoid duplication
+emit "declarations:DropDownIndexed"
+||
+ Object.getOwnPropertyNames(DropDownWidget.prototype).forEach(name => {
+ if (name !== "constructor" && name !== "dispatch") {
+ DropDownIndexedWidget.prototype[name] = DropDownWidget.prototype[name];
+ }
+ });
+||
+
+widget_defs("DropDownIndexed") {
+ labels("box button highlight");
+ // It is assumed that list content conforms to Array interface.
+ const "text_elt","$hmi_element//*[@inkscape:label='text'][1]";
+ | init_specific: function() {
+ choose{
+ // special case when used for language selection
+ when "count(arg) = 1 and arg[1]/@value = '#langs'" {
+ | this.text_elt = id("«$text_elt/@id»");
+ | this.content = langs.map(([lname,lcode]) => lname);
+ }
+ when "count(arg) = 0"{
+ if "not($text_elt[self::svg:use])"
+ error > No argument for HMI:DropDownIndexed widget id="«$hmi_element/@id»" and "text" labeled element is not a svg:use element
+ const "real_text_elt","$result_widgets[@id = $hmi_element/@id]//*[@original=$text_elt/@id]/svg:text";
+ | this.text_elt = id("«$real_text_elt/@id»");
+ const "from_list_id", "substring-after($text_elt/@xlink:href,'#')";
+ const "from_list", "$hmi_textlists[(@id | */@id) = $from_list_id]";
+ if "count($from_list) = 0"
+ error > HMI:DropDownIndexed widget id="«$hmi_element/@id»" "text" labeled element does not point to a svg:text owned by a HMI:List widget
+ | this.content = hmi_widgets["«$from_list/@id»"].texts;
+ }
+ otherwise {
+ | this.text_elt = id("«$text_elt/@id»");
+ | this.content = [
+ foreach "arg" | "«@value»",
+ | ];
+ }
+ }
+ | }
+}
\ No newline at end of file