# HG changeset patch # User Edouard Tisserant # Date 1608129864 -3600 # Node ID f475f39713aa94007f688a5f753be40262aac5d1 # Parent 9e172e4e50c7c676eaa8033c7bdbe4c0bb32a4c6 SVGHMI: change scroll buttons into single unicode triangle and center them. Use highlight rectangle duplicated and hidden to catch selection clicks so that the whole row is clickable. diff -r 9e172e4e50c7 -r f475f39713aa svghmi/gen_index_xhtml.xslt --- a/svghmi/gen_index_xhtml.xslt Tue Dec 15 13:43:21 2020 +0100 +++ b/svghmi/gen_index_xhtml.xslt Wed Dec 16 15:44:24 2020 +0100 @@ -3062,6 +3062,12 @@ </xsl:text> </xsl:template> <xsl:template mode="widget_class" match="widget[@type='DropDown']"> + <xsl:text> function numb_event(e) { +</xsl:text> + <xsl:text> e.stopPropagation(); +</xsl:text> + <xsl:text> } +</xsl:text> <xsl:text> class DropDownWidget extends Widget{ </xsl:text> <xsl:text> dispatch(value) { @@ -3082,11 +3088,11 @@ </xsl:text> <xsl:text> // Compute margins </xsl:text> - <xsl:text> let text_bbox = this.text_elt.getBBox(); -</xsl:text> - <xsl:text> let lmargin = text_bbox.x - this.box_bbox.x; -</xsl:text> - <xsl:text> let tmargin = text_bbox.y - this.box_bbox.y; + <xsl:text> this.text_bbox = this.text_elt.getBBox(); +</xsl:text> + <xsl:text> let lmargin = this.text_bbox.x - this.box_bbox.x; +</xsl:text> + <xsl:text> let tmargin = this.text_bbox.y - this.box_bbox.y; </xsl:text> <xsl:text> this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); </xsl:text> @@ -3118,6 +3124,8 @@ </xsl:text> <xsl:text> this.opened = false; </xsl:text> + <xsl:text> this.clickables = []; +</xsl:text> <xsl:text> } </xsl:text> <xsl:text> on_button_click() { @@ -3180,13 +3188,13 @@ </xsl:text> <xsl:text> let count = 1; </xsl:text> - <xsl:text> let txt = this.text_elt; + <xsl:text> let txt = this.text_elt; </xsl:text> <xsl:text> let first = txt.firstElementChild; </xsl:text> <xsl:text> // Real world (pixels) boundaries of current page </xsl:text> - <xsl:text> let bounds = svg_root.getBoundingClientRect(); + <xsl:text> let bounds = svg_root.getBoundingClientRect(); </xsl:text> <xsl:text> this.lift = 0; </xsl:text> @@ -3266,7 +3274,7 @@ </xsl:text> <xsl:text> // inhibit events not targetting spans (menu items) </xsl:text> - <xsl:text> if(e.target.parentNode !== this.text_elt){ + <xsl:text> if([this.text_elt, this.element].indexOf(e.target.parentNode) == -1){ </xsl:text> <xsl:text> e.stopPropagation(); </xsl:text> @@ -3284,12 +3292,18 @@ </xsl:text> <xsl:text> // Stop hogging all click events </xsl:text> + <xsl:text> svg_root.removeEventListener("pointerdown", numb_event, true); +</xsl:text> + <xsl:text> svg_root.removeEventListener("pointerup", numb_event, true); +</xsl:text> <xsl:text> svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); </xsl:text> <xsl:text> // Restore position and sixe of widget elements </xsl:text> <xsl:text> this.reset_text(); </xsl:text> + <xsl:text> this.reset_clickables(); +</xsl:text> <xsl:text> this.reset_box(); </xsl:text> <xsl:text> // Put the button back in place @@ -3306,11 +3320,55 @@ </xsl:text> <xsl:text> } </xsl:text> + <xsl:text> // Make item (text span) clickable by overlaying a rectangle on top of it +</xsl:text> + <xsl:text> make_clickable(span, func) { +</xsl:text> + <xsl:text> let txt = this.text_elt; +</xsl:text> + <xsl:text> let first = txt.firstElementChild; +</xsl:text> + <xsl:text> let original_text_y = this.text_bbox.y; +</xsl:text> + <xsl:text> let highlight = this.highlight_elt; +</xsl:text> + <xsl:text> let original_h_y = highlight.getBBox().y; +</xsl:text> + <xsl:text> let clickable = highlight.cloneNode(); +</xsl:text> + <xsl:text> let yoffset = span.getBBox().y - original_text_y; +</xsl:text> + <xsl:text> clickable.setAttribute("y", original_h_y + yoffset); +</xsl:text> + <xsl:text> clickable.style.pointerEvents = "bounding-box"; +</xsl:text> + <xsl:text> clickable.style.visibility = "hidden"; +</xsl:text> + <xsl:text> //clickable.onclick = () => alert("love JS"); +</xsl:text> + <xsl:text> clickable.onclick = func; +</xsl:text> + <xsl:text> this.element.appendChild(clickable); +</xsl:text> + <xsl:text> this.clickables.push(clickable) +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> reset_clickables() { +</xsl:text> + <xsl:text> while(this.clickables.length){ +</xsl:text> + <xsl:text> this.element.removeChild(this.clickables.pop()); +</xsl:text> + <xsl:text> } +</xsl:text> + <xsl:text> } +</xsl:text> <xsl:text> // Set text content when content is smaller than menu (no scrolling) </xsl:text> <xsl:text> set_complete_text(){ </xsl:text> - <xsl:text> let spans = this.text_elt.children; + <xsl:text> let spans = this.text_elt.children; </xsl:text> <xsl:text> let c = 0; </xsl:text> @@ -3320,7 +3378,9 @@ </xsl:text> <xsl:text> span.textContent = item; </xsl:text> - <xsl:text> span.onclick = (evt) => this.bound_on_selection_click(c); + <xsl:text> let sel = c; +</xsl:text> + <xsl:text> this.make_clickable(span, (evt) => this.bound_on_selection_click(sel)); </xsl:text> <xsl:text> c++; </xsl:text> @@ -3338,7 +3398,7 @@ </xsl:text> <xsl:text> let contentlength = this.content.length; </xsl:text> - <xsl:text> let spans = this.text_elt.children; + <xsl:text> let spans = this.text_elt.children; </xsl:text> <xsl:text> let spanslength = spans.length; </xsl:text> @@ -3352,7 +3412,7 @@ </xsl:text> <xsl:text> this.menu_offset = Math.min( </xsl:text> - <xsl:text> contentlength - spans.length + 1, + <xsl:text> contentlength - spans.length + 1, </xsl:text> <xsl:text> this.menu_offset + spanslength); </xsl:text> @@ -3360,12 +3420,14 @@ </xsl:text> <xsl:text> this.menu_offset = Math.max( </xsl:text> - <xsl:text> 0, + <xsl:text> 0, </xsl:text> <xsl:text> this.menu_offset - spanslength); </xsl:text> <xsl:text> } </xsl:text> + <xsl:text> this.reset_clickables(); +</xsl:text> <xsl:text> this.set_partial_text(); </xsl:text> <xsl:text> } @@ -3376,7 +3438,7 @@ </xsl:text> <xsl:text> set_partial_text(){ </xsl:text> - <xsl:text> let spans = this.text_elt.children; + <xsl:text> let spans = this.text_elt.children; </xsl:text> <xsl:text> let contentlength = this.content.length; </xsl:text> @@ -3384,25 +3446,37 @@ </xsl:text> <xsl:text> let i = this.menu_offset, c = 0; </xsl:text> + <xsl:text> let m = this.box_bbox; +</xsl:text> <xsl:text> while(c < spanslength){ </xsl:text> <xsl:text> let span=spans[c]; </xsl:text> + <xsl:text> let onclickfunc; +</xsl:text> <xsl:text> // backward jump only present if not exactly at start </xsl:text> <xsl:text> if(c == 0 && i != 0){ </xsl:text> - <xsl:text> span.textContent = "↑ ↑ ↑"; -</xsl:text> - <xsl:text> span.onclick = this.bound_on_backward_click; + <xsl:text> span.textContent = "▲"; +</xsl:text> + <xsl:text> onclickfunc = this.bound_on_backward_click; +</xsl:text> + <xsl:text> let o = span.getBBox(); +</xsl:text> + <xsl:text> span.setAttribute("dx", (m.width - o.width)/2); </xsl:text> <xsl:text> // presence of forward jump when not right at the end </xsl:text> <xsl:text> }else if(c == spanslength-1 && i < contentlength - 1){ </xsl:text> - <xsl:text> span.textContent = "↓ ↓ ↓"; -</xsl:text> - <xsl:text> span.onclick = this.bound_on_forward_click; + <xsl:text> span.textContent = "▼"; +</xsl:text> + <xsl:text> onclickfunc = this.bound_on_forward_click; +</xsl:text> + <xsl:text> let o = span.getBBox(); +</xsl:text> + <xsl:text> span.setAttribute("dx", (m.width - o.width)/2); </xsl:text> <xsl:text> // otherwise normal content </xsl:text> @@ -3412,12 +3486,16 @@ </xsl:text> <xsl:text> let sel = i; </xsl:text> - <xsl:text> span.onclick = (evt) => this.bound_on_selection_click(sel); + <xsl:text> onclickfunc = (evt) => this.bound_on_selection_click(sel); +</xsl:text> + <xsl:text> span.removeAttribute("dx"); </xsl:text> <xsl:text> i++; </xsl:text> <xsl:text> } </xsl:text> + <xsl:text> this.make_clickable(span, onclickfunc); +</xsl:text> <xsl:text> c++; </xsl:text> <xsl:text> } @@ -3478,6 +3556,10 @@ </xsl:text> <xsl:text> // disable interaction with background </xsl:text> + <xsl:text> svg_root.addEventListener("pointerdown", numb_event, true); +</xsl:text> + <xsl:text> svg_root.addEventListener("pointerup", numb_event, true); +</xsl:text> <xsl:text> svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); </xsl:text> <xsl:text> // mark as open @@ -3490,16 +3572,18 @@ </xsl:text> <xsl:text> reset_text(){ </xsl:text> - <xsl:text> let txt = this.text_elt; + <xsl:text> let txt = this.text_elt; </xsl:text> <xsl:text> let first = txt.firstElementChild; </xsl:text> <xsl:text> // remove attribute eventually added to first text line while opening </xsl:text> - <xsl:text> first.removeAttribute("onclick"); + <xsl:text> first.onclick = null; </xsl:text> <xsl:text> first.removeAttribute("dy"); </xsl:text> + <xsl:text> first.removeAttribute("dx"); +</xsl:text> <xsl:text> // keep only the first line of text </xsl:text> <xsl:text> for(let span of Array.from(txt.children).slice(1)){ @@ -3538,11 +3622,11 @@ </xsl:text> <xsl:text> let b = this.box_elt; </xsl:text> - <xsl:text> b.x.baseVal.value = m.x - lmargin; + <xsl:text> // b.x.baseVal.value = m.x - lmargin; </xsl:text> <xsl:text> b.y.baseVal.value = m.y - tmargin; </xsl:text> - <xsl:text> b.width.baseVal.value = 2 * lmargin + m.width; + <xsl:text> // b.width.baseVal.value = 2 * lmargin + m.width; </xsl:text> <xsl:text> b.height.baseVal.value = 2 * tmargin + m.height; </xsl:text> @@ -3556,7 +3640,7 @@ <xsl:call-template name="defs_by_labels"> <xsl:with-param name="hmi_element" select="$hmi_element"/> <xsl:with-param name="labels"> - <xsl:text>text box button</xsl:text> + <xsl:text>text box button highlight</xsl:text> </xsl:with-param> </xsl:call-template> <xsl:text> // It is assumed that list content conforms to Array interface. diff -r 9e172e4e50c7 -r f475f39713aa svghmi/widget_dropdown.ysl2 --- a/svghmi/widget_dropdown.ysl2 Tue Dec 15 13:43:21 2020 +0100 +++ b/svghmi/widget_dropdown.ysl2 Wed Dec 16 15:44:24 2020 +0100 @@ -2,6 +2,9 @@ template "widget[@type='DropDown']", mode="widget_class"{ || + function numb_event(e) { + e.stopPropagation(); + } class DropDownWidget extends Widget{ dispatch(value) { if(!this.opened) this.set_selection(value); @@ -12,9 +15,9 @@ this.box_bbox = this.box_elt.getBBox() // Compute margins - let text_bbox = this.text_elt.getBBox(); - let lmargin = text_bbox.x - this.box_bbox.x; - let tmargin = text_bbox.y - this.box_bbox.y; + this.text_bbox = this.text_elt.getBBox(); + let lmargin = this.text_bbox.x - this.box_bbox.x; + let tmargin = this.text_bbox.y - this.box_bbox.y; this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); // Index of first visible element in the menu, when opened @@ -30,6 +33,7 @@ this.bound_on_backward_click = this.on_backward_click.bind(this); this.bound_on_forward_click = this.on_forward_click.bind(this); this.opened = false; + this.clickables = []; } on_button_click() { this.open(); @@ -61,10 +65,10 @@ } grow_text(up_to) { let count = 1; - let txt = this.text_elt; + let txt = this.text_elt; let first = txt.firstElementChild; // Real world (pixels) boundaries of current page - let bounds = svg_root.getBoundingClientRect(); + let bounds = svg_root.getBoundingClientRect(); this.lift = 0; while(count < up_to) { let next = first.cloneNode(); @@ -104,7 +108,7 @@ } close_on_click_elsewhere(e) { // inhibit events not targetting spans (menu items) - if(e.target.parentNode !== this.text_elt){ + if([this.text_elt, this.element].indexOf(e.target.parentNode) == -1){ e.stopPropagation(); // close menu in case click is outside box if(e.target !== this.box_elt) @@ -113,9 +117,12 @@ } close(){ // Stop hogging all click events + svg_root.removeEventListener("pointerdown", numb_event, true); + svg_root.removeEventListener("pointerup", numb_event, true); svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); // Restore position and sixe of widget elements this.reset_text(); + this.reset_clickables(); this.reset_box(); // Put the button back in place this.element.appendChild(this.button_elt); @@ -124,15 +131,37 @@ // Dispatch last cached value this.apply_cache(); } + // Make item (text span) clickable by overlaying a rectangle on top of it + make_clickable(span, func) { + let txt = this.text_elt; + let first = txt.firstElementChild; + let original_text_y = this.text_bbox.y; + let highlight = this.highlight_elt; + let original_h_y = highlight.getBBox().y; + let clickable = highlight.cloneNode(); + let yoffset = span.getBBox().y - original_text_y; + clickable.setAttribute("y", original_h_y + yoffset); + clickable.style.pointerEvents = "bounding-box"; + clickable.style.visibility = "hidden"; + //clickable.onclick = () => alert("love JS"); + clickable.onclick = func; + this.element.appendChild(clickable); + this.clickables.push(clickable) + } + reset_clickables() { + while(this.clickables.length){ + this.element.removeChild(this.clickables.pop()); + } + } // Set text content when content is smaller than menu (no scrolling) set_complete_text(){ - let spans = this.text_elt.children; + let spans = this.text_elt.children; let c = 0; for(let item of this.content){ let span=spans[c]; span.textContent = item; let sel = c; - span.onclick = (evt) => this.bound_on_selection_click(sel); + this.make_clickable(span, (evt) => this.bound_on_selection_click(sel)); c++; } } @@ -141,46 +170,55 @@ // true : downward, higher value scroll(forward){ let contentlength = this.content.length; - let spans = this.text_elt.children; + let spans = this.text_elt.children; let spanslength = spans.length; // reduce accounted menu size according to jumps if(this.menu_offset != 0) spanslength--; if(this.menu_offset < contentlength - 1) spanslength--; if(forward){ this.menu_offset = Math.min( - contentlength - spans.length + 1, + contentlength - spans.length + 1, this.menu_offset + spanslength); }else{ this.menu_offset = Math.max( - 0, + 0, this.menu_offset - spanslength); } + this.reset_clickables(); this.set_partial_text(); } // Setup partial view text content // with jumps at first and last entry when appropriate set_partial_text(){ - let spans = this.text_elt.children; + let spans = this.text_elt.children; let contentlength = this.content.length; let spanslength = spans.length; let i = this.menu_offset, c = 0; + let m = this.box_bbox; while(c < spanslength){ let span=spans[c]; + let onclickfunc; // backward jump only present if not exactly at start if(c == 0 && i != 0){ - span.textContent = "↑ ↑ ↑"; - span.onclick = this.bound_on_backward_click; + span.textContent = "â–²"; + onclickfunc = this.bound_on_backward_click; + let o = span.getBBox(); + span.setAttribute("dx", (m.width - o.width)/2); // presence of forward jump when not right at the end }else if(c == spanslength-1 && i < contentlength - 1){ - span.textContent = "↓ ↓ ↓"; - span.onclick = this.bound_on_forward_click; + span.textContent = "â–¼"; + onclickfunc = this.bound_on_forward_click; + let o = span.getBBox(); + span.setAttribute("dx", (m.width - o.width)/2); // otherwise normal content }else{ span.textContent = this.content[i]; let sel = i; - span.onclick = (evt) => this.bound_on_selection_click(sel); + onclickfunc = (evt) => this.bound_on_selection_click(sel); + span.removeAttribute("dx"); i++; } + this.make_clickable(span, onclickfunc); c++; } } @@ -211,17 +249,20 @@ // Rise widget to top by moving it to last position among siblings this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); // disable interaction with background + svg_root.addEventListener("pointerdown", numb_event, true); + svg_root.addEventListener("pointerup", numb_event, true); svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); // mark as open this.opened = true; } // Put text element in normalized state reset_text(){ - let txt = this.text_elt; + let txt = this.text_elt; let first = txt.firstElementChild; // remove attribute eventually added to first text line while opening - first.removeAttribute("onclick"); + first.onclick = null; first.removeAttribute("dy"); + first.removeAttribute("dx"); // keep only the first line of text for(let span of Array.from(txt.children).slice(1)){ txt.removeChild(span) @@ -241,17 +282,18 @@ let [lmargin, tmargin] = this.margins; let m = this.text_elt.getBBox(); let b = this.box_elt; - b.x.baseVal.value = m.x - lmargin; + // b.x.baseVal.value = m.x - lmargin; b.y.baseVal.value = m.y - tmargin; - b.width.baseVal.value = 2 * lmargin + m.width; + // b.width.baseVal.value = 2 * lmargin + m.width; b.height.baseVal.value = 2 * tmargin + m.height; } } || } + template "widget[@type='DropDown']", mode="widget_defs" { param "hmi_element"; - labels("text box button"); + labels("text box button highlight"); || // It is assumed that list content conforms to Array interface. content: [ diff -r 9e172e4e50c7 -r f475f39713aa tests/svghmi/svghmi_0@svghmi/svghmi.svg --- a/tests/svghmi/svghmi_0@svghmi/svghmi.svg Tue Dec 15 13:43:21 2020 +0100 +++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Wed Dec 16 15:44:24 2020 +0100 @@ -197,19 +197,20 @@ inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:document-units="px" - inkscape:current-layer="g443" + inkscape:current-layer="g14237" showgrid="false" units="px" - inkscape:zoom="0.77167689" - inkscape:cx="379.87087" - inkscape:cy="462.91635" - inkscape:window-width="1848" - inkscape:window-height="1016" - inkscape:window-x="72" + inkscape:zoom="0.54565796" + inkscape:cx="147.6698" + inkscape:cy="180.09341" + inkscape:window-width="1600" + inkscape:window-height="836" + inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" showguides="true" - inkscape:guide-bbox="true" /> + inkscape:guide-bbox="true" + inkscape:snap-global="false" /> <use x="0" y="0" @@ -2406,6 +2407,16 @@ rx="2.4558709" ry="2.4558709" inkscape:label="box" /> + <rect + inkscape:label="highlight" + ry="2.4558709" + rx="2.4558709" + y="943.10553" + x="864.00842" + height="92.71212" + width="391.99988" + id="rect5497" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#0000ff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.75419331;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> <text id="text14183" y="1011.9975" @@ -2442,7 +2453,7 @@ <g inkscape:label="HMI:Input@/SELECTION" id="g446" - transform="matrix(0.28590269,0,0,0.28590269,1047.3881,408.87609)"> + transform="matrix(0.28590269,0,0,0.28590269,85.246911,560.98603)"> <text xml:space="preserve" style="font-style:normal;font-weight:normal;font-size:160px;line-height:125%;font-family:sans-serif;text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" @@ -2519,7 +2530,7 @@ inkscape:transform-center-x="1.0089177e-06" /> </g> <g - transform="matrix(0.57180538,0,0,0.57180538,417.18774,31.574523)" + transform="matrix(0.57180538,0,0,0.57180538,-522.96165,161.69266)" id="g443" inkscape:label="HMI:Button@/SELECTION" style="stroke-width:1"> @@ -2542,7 +2553,7 @@ height="95.723877" width="245.44583" id="rect433" - style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fdfdfd;fill-opacity:1;fill-rule:nonzero;stroke:#ffd0b2;stroke-width:28.60938356;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fdfdfd;fill-opacity:1;fill-rule:nonzero;stroke:#ffd0b2;stroke-width:28.60938263;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> <g style="stroke-width:1" inkscape:label="text"