Marged default in wxPython4 wxPython4
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Wed, 23 Nov 2022 14:18:25 +0100
branchwxPython4
changeset 3688 c2992796a859
parent 3679 b6bca75bf3fa (current diff)
parent 3687 747ffdafbe31 (diff)
child 3692 a9b787d84846
Marged default in wxPython4
editors/TextViewer.py
svghmi/analyse_widget.xslt
svghmi/gen_index_xhtml.xslt
svghmi/svghmi.py
--- a/editors/TextViewer.py	Sun Nov 20 18:36:13 2022 +0100
+++ b/editors/TextViewer.py	Wed Nov 23 14:18:25 2022 +0100
@@ -130,8 +130,7 @@
         self.Editor.SetUseTabs(0)
 
         self.Editor.SetModEventMask(wx.stc.STC_MOD_BEFOREINSERT |
-                                    wx.stc.STC_MOD_BEFOREDELETE |
-                                    wx.stc.STC_PERFORMED_USER)
+                                    wx.stc.STC_MOD_BEFOREDELETE)
 
         self.Bind(wx.stc.EVT_STC_STYLENEEDED, self.OnStyleNeeded, self.Editor)
         self.Editor.Bind(wx.stc.EVT_STC_MARGINCLICK, self.OnMarginClick)
@@ -221,25 +220,24 @@
         self.SearchResults = None
         self.CurrentFindHighlight = None
 
+    Buffering = "Off"
     def OnModification(self, event):
         if not self.DisableEvents:
             mod_type = event.GetModificationType()
             if mod_type & wx.stc.STC_MOD_BEFOREINSERT:
                 if self.CurrentAction is None:
-                    self.StartBuffering()
+                    self.Buffering = "ShouldStart"
                 elif self.CurrentAction[0] != "Add" or self.CurrentAction[1] != event.GetPosition() - 1:
-                    self.Controler.EndBuffering()
-                    self.StartBuffering()
+                    self.Buffering = "ShouldRestart"
                 self.CurrentAction = ("Add", event.GetPosition())
-                wx.CallAfter(self.RefreshModel)
+                self.RefreshModelAfter()
             elif mod_type & wx.stc.STC_MOD_BEFOREDELETE:
                 if self.CurrentAction is None:
-                    self.StartBuffering()
+                    self.Buffering = "ShouldStart"
                 elif self.CurrentAction[0] != "Delete" or self.CurrentAction[1] != event.GetPosition() + 1:
-                    self.Controler.EndBuffering()
-                    self.StartBuffering()
+                    self.Buffering = "ShouldRestart"
                 self.CurrentAction = ("Delete", event.GetPosition())
-                wx.CallAfter(self.RefreshModel)
+                self.RefreshModelAfter()
         event.Skip()
 
     def OnDoDrop(self, event):
@@ -387,7 +385,7 @@
             elif values[3] == self.TagName:
                 self.ResetBuffer()
                 event.SetDragText(values[0])
-                wx.CallAfter(self.RefreshModel)
+                self.RefreshModelAfter()
             else:
                 message = _("Variable don't belong to this POU!")
             if message is not None:
@@ -437,10 +435,14 @@
             self.ParentWindow.RefreshFileMenu()
             self.ParentWindow.RefreshEditMenu()
 
+    def EndBuffering(self):
+        self.Controler.EndBuffering()
+
     def ResetBuffer(self):
         if self.CurrentAction is not None:
-            self.Controler.EndBuffering()
+            self.EndBuffering()
             self.CurrentAction = None
+            self.Buffering == "Off"
 
     def GetBufferState(self):
         if not self.Debug and self.TextSyntax != "ALL":
@@ -840,12 +842,29 @@
                 self.RemoveHighlight(*self.CurrentFindHighlight)
             self.CurrentFindHighlight = None
 
+    pending_model_refresh=False
     def RefreshModel(self):
+        self.pending_model_refresh=False
         self.RefreshJumpList()
         self.Colourise(0, -1)
+
+        if self.Buffering == "ShouldStart":
+            self.StartBuffering()
+            self.Buffering == "On"
+        elif self.Buffering == "ShouldRestart":
+            self.EndBuffering()
+            self.StartBuffering()
+            self.Buffering == "On"
+
         self.Controler.SetEditedElementText(self.TagName, self.GetText())
         self.ResetSearchResults()
 
+    def RefreshModelAfter(self):
+        if self.pending_model_refresh:
+            return
+        self.pending_model_refresh=True
+        wx.CallAfter(self.RefreshModel)
+
     def OnKeyDown(self, event):
         key = event.GetKeyCode()
         if self.Controler is not None:
--- a/exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg	Sun Nov 20 18:36:13 2022 +0100
+++ b/exemples/svghmi_jumps/svghmi_0@svghmi/svghmi.svg	Wed Nov 23 14:18:25 2022 +0100
@@ -25,7 +25,7 @@
         <dc:format>image/svg+xml</dc:format>
         <dc:type
            rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
+        <dc:title />
       </cc:Work>
     </rdf:RDF>
   </metadata>
@@ -40,17 +40,17 @@
      guidetolerance="10"
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
-     inkscape:window-width="1600"
-     inkscape:window-height="836"
+     inkscape:window-width="1850"
+     inkscape:window-height="1036"
      id="namedview4"
      showgrid="false"
-     inkscape:zoom="0.23177389"
-     inkscape:cx="1999.5317"
-     inkscape:cy="-682.74047"
+     inkscape:zoom="0.46354778"
+     inkscape:cx="-544.27948"
+     inkscape:cy="655.56978"
      inkscape:window-x="0"
      inkscape:window-y="27"
      inkscape:window-maximized="1"
-     inkscape:current-layer="hmi0"
+     inkscape:current-layer="g2496"
      showguides="true"
      inkscape:guide-bbox="true"
      borderlayer="true"
@@ -67,7 +67,7 @@
      transform="translate(1320,1520)"
      width="100%"
      height="100%"
-     inkscape:label="HMI:Page:RelativePage@/FB_ZERO" />
+     inkscape:label="HMI:Page:RelativePage:p=6@p=page_number@/FB_ZERO" />
   <use
      sodipodi:insensitive="true"
      x="0"
@@ -77,7 +77,7 @@
      transform="translate(2640,759.99998)"
      width="100%"
      height="100%"
-     inkscape:label="HMI:Page:Relative" />
+     inkscape:label="HMI:Page:Relative:p=5@p=page_number" />
   <use
      x="0"
      y="0"
@@ -86,10 +86,10 @@
      transform="translate(3940,-2.1367187e-5)"
      width="100%"
      height="100%"
-     inkscape:label="HMI:Page:Conditional"
+     inkscape:label="HMI:Page:Conditional:p=4@p=page_number"
      sodipodi:insensitive="true" />
   <use
-     inkscape:label="HMI:Page:Unconditional"
+     inkscape:label="HMI:Page:Unconditional:p=3@p=page_number"
      height="100%"
      width="100%"
      transform="translate(2640,-2.1367187e-5)"
@@ -99,7 +99,7 @@
      x="0"
      sodipodi:insensitive="true" />
   <use
-     inkscape:label="HMI:Page:AbsolutePage"
+     inkscape:label="HMI:Page:AbsolutePage:p=2@p=page_number"
      height="100%"
      width="100%"
      transform="translate(1320,759.99998)"
@@ -116,7 +116,7 @@
      transform="translate(1320,-2.1367187e-5)"
      width="100%"
      height="100%"
-     inkscape:label="HMI:Page:Home"
+     inkscape:label="HMI:Page:Home:p=1@p=page_number"
      sodipodi:insensitive="true" />
   <text
      id="text837"
@@ -621,6 +621,18 @@
          id="tspan1849"
          x="-1166.3386"
          y="598.98303">HMI:Jump:RelativePage</tspan></text>
+    <text
+       inkscape:label="HMI:Display@page_number"
+       xml:space="preserve"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:50.03883362px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="-75.162941"
+       y="702.24023"
+       id="text296"><tspan
+         sodipodi:role="line"
+         id="tspan294"
+         x="-75.162941"
+         y="702.24023"
+         style="stroke-width:1px">0</tspan></text>
   </g>
   <g
      id="g2585"
@@ -1200,7 +1212,7 @@
     <g
        id="g1711"
        inkscape:label="MYNODE:+1"
-       transform="matrix(1.3729714,0,0,1.3729714,-299.47126,-800.64485)"
+       transform="matrix(1.3729714,0,0,1.3729714,-359.47134,-800.64485)"
        style="stroke-width:0.7283473">
       <rect
          rx="15.554536"
@@ -1226,7 +1238,7 @@
     <g
        inkscape:label="MYNODE:3"
        id="g1837"
-       transform="translate(665.54481,-11.353461)">
+       transform="translate(620.54487,-11.353461)">
       <g
          transform="translate(1466.6549,2099.2529)"
          inkscape:label="HMI:Jump:RelativePage@/FB_TWO@enable=/FB_TWO/SOME_BOOL#enable"
@@ -1286,7 +1298,7 @@
       </g>
     </g>
     <g
-       transform="translate(342.39289,-11.353461)"
+       transform="translate(312.39295,-11.353461)"
        id="g1823"
        inkscape:label="MYNODE:2">
       <g
@@ -1350,7 +1362,7 @@
     <g
        inkscape:label="MYNODE:1"
        id="g1717"
-       transform="translate(19.240974,-11.353461)">
+       transform="translate(4.2410198,-11.353461)">
       <g
          transform="translate(1466.6549,2099.2529)"
          inkscape:label="HMI:Jump:RelativePage@/FB_ZERO@enable=/FB_ZERO/SOME_BOOL#enable"
@@ -1815,4 +1827,23 @@
        x="44.283585"
        y="-39.185921"
        id="tspan2966">- Press Ctrl+X to edit SVG elements directly with XML editor</tspan></text>
+  <g
+     transform="translate(-4020,2.1367187e-5)"
+     inkscape:label="HMI:VarInit:0@page_number"
+     id="g304">
+    <text
+       id="text302"
+       y="-108.39357"
+       x="3726.6924"
+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.66666603px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       xml:space="preserve"><tspan
+         y="-108.39357"
+         x="3726.6924"
+         id="tspan298"
+         sodipodi:role="line">declaration of user_level HMI local variable</tspan><tspan
+         id="tspan300"
+         y="-85.060234"
+         x="3726.6924"
+         sodipodi:role="line">(not a PLC variable)</tspan></text>
+  </g>
 </svg>
--- a/svghmi/analyse_widget.xslt	Sun Nov 20 18:36:13 2022 +0100
+++ b/svghmi/analyse_widget.xslt	Wed Nov 23 14:18:25 2022 +0100
@@ -854,6 +854,46 @@
       <xsl:text>Value to display</xsl:text>
     </path>
   </xsl:template>
+  <xsl:template match="widget[@type='Page']" mode="widget_desc">
+    <type>
+      <xsl:value-of select="@type"/>
+    </type>
+    <longdesc>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>Arguments are either:
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>- XXX reference path TODO
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>- name=value: setting variable with literal value.
+</xsl:text>
+      <xsl:text>- name=other_name: copy variable content into another
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>Exemples:
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>HMI:Page:notify=1@notify=/PLCVAR
+</xsl:text>
+      <xsl:text>HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+    </longdesc>
+    <shortdesc>
+      <xsl:text>Page </xsl:text>
+    </shortdesc>
+  </xsl:template>
   <xsl:template match="widget[@type='PathSlider']" mode="widget_desc">
     <type>
       <xsl:value-of select="@type"/>
--- a/svghmi/detachable_pages.ysl2	Sun Nov 20 18:36:13 2022 +0100
+++ b/svghmi/detachable_pages.ysl2	Wed Nov 23 14:18:25 2022 +0100
@@ -137,6 +137,10 @@
 const "_detachable_elements", "func:detachable_elements($hmi_pages | $keypads)";
 const "detachable_elements", "$_detachable_elements[not(ancestor::*/@id = $_detachable_elements/@id)]";
 
+emit "declarations:page-class" {
+    | class PageWidget extends Widget{}
+}
+
 emit "declarations:detachable-elements" {
     |
     | var detachable_elements = {
@@ -165,8 +169,13 @@
 
     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, $desc/path/@value)]";
+        "$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
@@ -178,19 +187,19 @@
            ancestor-or-self::*[@id = $detachable_elements/@id]""";
 
     |   "«$pagename»": {
-    //|     widget: hmi_widgets["«@id»"],
     |     bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
-    if "$desc/path/@value" {
-        if "count($desc/path/@index)=0"
-            warning > Page id="«$page/@id»" : No match for path "«$desc/path/@value»" in HMI tree
-    |     page_index: «$desc/path/@index»,
-    |     page_class: "«$indexed_hmitree/*[@hmipath = $desc/path/@value]/@class»",
+    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, $desc/path/@value)";
+                value "func:is_descendant_path(@value, $page_root_path/@value)";
                 if "position()!=last()" > ,
             }
     |         [hmi_widgets["«@id»"], [«$widget_paths_relativeness»]]`if "position()!=last()" > ,`
--- a/svghmi/gen_index_xhtml.xslt	Sun Nov 20 18:36:13 2022 +0100
+++ b/svghmi/gen_index_xhtml.xslt	Wed Nov 23 14:18:25 2022 +0100
@@ -734,6 +734,21 @@
   </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)]"/>
+  <declarations:page-class/>
+  <xsl:template match="declarations:page-class">
+    <xsl:text>
+</xsl:text>
+    <xsl:text>/* </xsl:text>
+    <xsl:value-of select="local-name()"/>
+    <xsl:text> */
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+    <xsl:text>class PageWidget extends Widget{}
+</xsl:text>
+    <xsl:text>
+</xsl:text>
+  </xsl:template>
   <declarations:detachable-elements/>
   <xsl:template match="declarations:detachable-elements">
     <xsl:text>
@@ -787,7 +802,15 @@
     <xsl:variable name="page_all_elements" select="func:all_related_elements($page)"/>
     <xsl:variable name="all_page_widgets" select="$hmi_widgets[@id = $page_all_elements/@id and @id != $page/@id]"/>
     <xsl:variable name="page_managed_widgets" select="$all_page_widgets[not(@id=$in_forEach_widget_ids)]"/>
-    <xsl:variable name="page_relative_widgets" select="$page_managed_widgets[func:is_descendant_path(func:widget(@id)/path/@value, $desc/path/@value)]"/>
+    <xsl:variable name="page_root_path" select="$desc/path[not(@assign)]"/>
+    <xsl:if test="count($page_root_path)&gt;1">
+      <xsl:message terminate="yes">
+        <xsl:text>Page id="</xsl:text>
+        <xsl:value-of select="$page/@id"/>
+        <xsl:text>" : only one root path can be declared</xsl:text>
+      </xsl:message>
+    </xsl:if>
+    <xsl:variable name="page_relative_widgets" select="$page_managed_widgets[func:is_descendant_path(func:widget(@id)/path/@value, $page_root_path/@value)]"/>
     <xsl:variable name="sumarized_page" select="func:sumarized_elements($page_all_elements)"/>
     <xsl:variable name="required_detachables" select="$sumarized_page/&#10;           ancestor-or-self::*[@id = $detachable_elements/@id]"/>
     <xsl:text>  "</xsl:text>
@@ -804,31 +827,35 @@
     <xsl:value-of select="$p/@h"/>
     <xsl:text>],
 </xsl:text>
-    <xsl:if test="$desc/path/@value">
-      <xsl:if test="count($desc/path/@index)=0">
+    <xsl:if test="count($page_root_path)=1">
+      <xsl:if test="count($page_root_path/@index)=0">
         <xsl:message terminate="no">
           <xsl:text>Page id="</xsl:text>
           <xsl:value-of select="$page/@id"/>
           <xsl:text>" : No match for path "</xsl:text>
-          <xsl:value-of select="$desc/path/@value"/>
+          <xsl:value-of select="$page_root_path/@value"/>
           <xsl:text>" in HMI tree</xsl:text>
         </xsl:message>
       </xsl:if>
       <xsl:text>    page_index: </xsl:text>
-      <xsl:value-of select="$desc/path/@index"/>
+      <xsl:value-of select="$page_root_path/@index"/>
       <xsl:text>,
 </xsl:text>
       <xsl:text>    page_class: "</xsl:text>
-      <xsl:value-of select="$indexed_hmitree/*[@hmipath = $desc/path/@value]/@class"/>
+      <xsl:value-of select="$indexed_hmitree/*[@hmipath = $page_root_path/@value]/@class"/>
       <xsl:text>",
 </xsl:text>
     </xsl:if>
     <xsl:text>    widgets: [
 </xsl:text>
+    <xsl:text>        [hmi_widgets["</xsl:text>
+    <xsl:value-of select="$page/@id"/>
+    <xsl:text>"], []],
+</xsl:text>
     <xsl:for-each select="$page_managed_widgets">
       <xsl:variable name="widget_paths_relativeness">
         <xsl:for-each select="func:widget(@id)/path">
-          <xsl:value-of select="func:is_descendant_path(@value, $desc/path/@value)"/>
+          <xsl:value-of select="func:is_descendant_path(@value, $page_root_path/@value)"/>
           <xsl:if test="position()!=last()">
             <xsl:text>,</xsl:text>
           </xsl:if>
@@ -1440,7 +1467,7 @@
     <xsl:text>,{
 </xsl:text>
     <xsl:if test="$widget/@enable_expr">
-      <xsl:text>      assignments: [],
+      <xsl:text>      enable_assignments: [],
 </xsl:text>
       <xsl:text>      compute_enable: function(value, oldval, varnum) {
 </xsl:text>
@@ -1456,13 +1483,13 @@
             <xsl:if test="$varid = generate-id()">
               <xsl:text>          if(varnum == </xsl:text>
               <xsl:value-of select="$varnum"/>
-              <xsl:text>) this.assignments[</xsl:text>
+              <xsl:text>) this.enable_assignments[</xsl:text>
               <xsl:value-of select="position()-1"/>
               <xsl:text>] = value;
 </xsl:text>
               <xsl:text>          let </xsl:text>
               <xsl:value-of select="@assign"/>
-              <xsl:text> = this.assignments[</xsl:text>
+              <xsl:text> = this.enable_assignments[</xsl:text>
               <xsl:value-of select="position()-1"/>
               <xsl:text>];
 </xsl:text>
@@ -2388,7 +2415,9 @@
     </xsl:message>
   </xsl:template>
   <xsl:variable name="included_ids" select="$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id"/>
+  <xsl:variable name="page_ids" select="$parsed_widgets/widget[@type = 'Page']/@id"/>
   <xsl:variable name="hmi_widgets" select="$hmi_elements[@id = $included_ids]"/>
+  <xsl:variable name="page_widgets" select="$hmi_elements[@id = $page_ids]"/>
   <xsl:variable name="result_widgets" select="$result_svg_ns//*[@id = $hmi_widgets/@id]"/>
   <declarations:hmi-elements/>
   <xsl:template match="declarations:hmi-elements">
@@ -2402,7 +2431,7 @@
 </xsl:text>
     <xsl:text>var hmi_widgets = {
 </xsl:text>
-    <xsl:apply-templates mode="hmi_widgets" select="$hmi_widgets"/>
+    <xsl:apply-templates mode="hmi_widgets" select="$hmi_widgets | $page_widgets"/>
     <xsl:text>}
 </xsl:text>
     <xsl:text>
@@ -6367,10 +6396,10 @@
       <xsl:variable name="target_page_path">
         <xsl:choose>
           <xsl:when test="arg">
-            <xsl:value-of select="$hmi_pages_descs[arg[1]/@value = $target_page_name]/path[1]/@value"/>
+            <xsl:value-of select="$hmi_pages_descs[arg[1]/@value = $target_page_name]/path[not(@assign)]/@value"/>
           </xsl:when>
           <xsl:otherwise>
-            <xsl:value-of select="$page_desc/path[1]/@value"/>
+            <xsl:value-of select="$page_desc/path[not(@assign)]/@value"/>
           </xsl:otherwise>
         </xsl:choose>
       </xsl:variable>
@@ -7195,6 +7224,128 @@
     <xsl:text>    ],
 </xsl:text>
   </xsl:template>
+  <xsl:template match="widget[@type='Page']" mode="widget_desc">
+    <type>
+      <xsl:value-of select="@type"/>
+    </type>
+    <longdesc>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>Arguments are either:
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>- XXX reference path TODO
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>- name=value: setting variable with literal value.
+</xsl:text>
+      <xsl:text>- name=other_name: copy variable content into another
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>"active"+"inactive" labeled elements can be provided to show feedback when pressed
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>Exemples:
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+      <xsl:text>HMI:Page:notify=1@notify=/PLCVAR
+</xsl:text>
+      <xsl:text>HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR
+</xsl:text>
+      <xsl:text>
+</xsl:text>
+    </longdesc>
+    <shortdesc>
+      <xsl:text>Page </xsl:text>
+    </shortdesc>
+  </xsl:template>
+  <xsl:template match="widget[@type='Page']" 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>    assignments: {},
+</xsl:text>
+    <xsl:text>    dispatch: function(value, oldval, varnum) {
+</xsl:text>
+    <xsl:variable name="widget" select="."/>
+    <xsl:for-each select="path">
+      <xsl:variable name="varid" select="generate-id()"/>
+      <xsl:variable name="varnum" select="position()-1"/>
+      <xsl:if test="@assign">
+        <xsl:for-each select="$widget/path[@assign]">
+          <xsl:if test="$varid = generate-id()">
+            <xsl:text>        if(varnum == </xsl:text>
+            <xsl:value-of select="$varnum"/>
+            <xsl:text>) this.assignments["</xsl:text>
+            <xsl:value-of select="@assign"/>
+            <xsl:text>"] = value;
+</xsl:text>
+          </xsl:if>
+        </xsl:for-each>
+      </xsl:if>
+    </xsl:for-each>
+    <xsl:text>    },
+</xsl:text>
+    <xsl:text>    assign: function() {
+</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="isVarName" select="regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]+$')"/>
+      <xsl:choose>
+        <xsl:when test="$isVarName">
+          <xsl:text>        const </xsl:text>
+          <xsl:value-of select="$value"/>
+          <xsl:text> = this.assignments["</xsl:text>
+          <xsl:value-of select="$value"/>
+          <xsl:text>"];
+</xsl:text>
+          <xsl:text>        if(</xsl:text>
+          <xsl:value-of select="$value"/>
+          <xsl:text> != undefined)
+</xsl:text>
+          <xsl:text>            this.apply_hmi_value(</xsl:text>
+          <xsl:value-of select="$index"/>
+          <xsl:text>, </xsl:text>
+          <xsl:value-of select="$value"/>
+          <xsl:text>);
+</xsl:text>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:text>        this.apply_hmi_value(</xsl:text>
+          <xsl:value-of select="$index"/>
+          <xsl:text>, </xsl:text>
+          <xsl:value-of select="$value"/>
+          <xsl:text>);
+</xsl:text>
+        </xsl:otherwise>
+      </xsl:choose>
+    </xsl:for-each>
+    <xsl:text>    },
+</xsl:text>
+  </xsl:template>
   <xsl:template match="widget[@type='PathSlider']" mode="widget_desc">
     <type>
       <xsl:value-of select="@type"/>
@@ -12128,29 +12279,37 @@
 </xsl:text>
           <xsl:text>
 </xsl:text>
-          <xsl:text>var screensaver_timer = null;
-</xsl:text>
-          <xsl:text>function reset_screensaver_timer() {
-</xsl:text>
-          <xsl:text>    if(screensaver_timer){
-</xsl:text>
-          <xsl:text>        window.clearTimeout(screensaver_timer);
+          <xsl:text>if(screensaver_delay){
+</xsl:text>
+          <xsl:text>    var screensaver_timer = null;
+</xsl:text>
+          <xsl:text>    function reset_screensaver_timer() {
+</xsl:text>
+          <xsl:text>        if(screensaver_timer){
+</xsl:text>
+          <xsl:text>            window.clearTimeout(screensaver_timer);
+</xsl:text>
+          <xsl:text>        }
+</xsl:text>
+          <xsl:text>        screensaver_timer = window.setTimeout(() =&gt; {
+</xsl:text>
+          <xsl:text>            switch_page("ScreenSaver");
+</xsl:text>
+          <xsl:text>            screensaver_timer = null;
+</xsl:text>
+          <xsl:text>        }, screensaver_delay*1000);
 </xsl:text>
           <xsl:text>    }
 </xsl:text>
-          <xsl:text>    screensaver_timer = window.setTimeout(() =&gt; {
-</xsl:text>
-          <xsl:text>        switch_page("ScreenSaver");
-</xsl:text>
-          <xsl:text>        screensaver_timer = null;
-</xsl:text>
-          <xsl:text>    }, screensaver_delay*1000);
+          <xsl:text>    document.body.addEventListener('pointerdown', reset_screensaver_timer);
+</xsl:text>
+          <xsl:text>    // initialize screensaver
+</xsl:text>
+          <xsl:text>    reset_screensaver_timer();
 </xsl:text>
           <xsl:text>}
 </xsl:text>
-          <xsl:text>if(screensaver_delay)
-</xsl:text>
-          <xsl:text>    document.body.addEventListener('pointerdown', reset_screensaver_timer);
+          <xsl:text>
 </xsl:text>
           <xsl:text>
 </xsl:text>
@@ -12314,6 +12473,12 @@
 </xsl:text>
           <xsl:text>
 </xsl:text>
+          <xsl:text>    // when entering a page, assignments are evaluated
+</xsl:text>
+          <xsl:text>    new_desc.widgets[0][0].assign();
+</xsl:text>
+          <xsl:text>
+</xsl:text>
           <xsl:text>    return true;
 </xsl:text>
           <xsl:text>};
@@ -12474,16 +12639,12 @@
 </xsl:text>
           <xsl:text>
 </xsl:text>
-          <xsl:text>// initialize screensaver
-</xsl:text>
-          <xsl:text>reset_screensaver_timer();
-</xsl:text>
-          <xsl:text>
-</xsl:text>
           <xsl:text>var reconnect_delay = 0;
 </xsl:text>
           <xsl:text>var periodic_reconnect_timer;
 </xsl:text>
+          <xsl:text>var force_reconnect = false;
+</xsl:text>
           <xsl:text>
 </xsl:text>
           <xsl:text>// Once connection established
@@ -12504,6 +12665,8 @@
 </xsl:text>
           <xsl:text>        periodic_reconnect_timer = window.setTimeout(() =&gt; {
 </xsl:text>
+          <xsl:text>            force_reconnect = true;
+</xsl:text>
           <xsl:text>            ws.close();
 </xsl:text>
           <xsl:text>            periodic_reconnect_timer = null;
@@ -12540,14 +12703,26 @@
 </xsl:text>
           <xsl:text>    ws = null;
 </xsl:text>
-          <xsl:text>    // reconect
-</xsl:text>
-          <xsl:text>    // TODO : add visible notification while waiting for reload
+          <xsl:text>    // Do not attempt to reconnect immediately in case:
+</xsl:text>
+          <xsl:text>    //    - connection was closed by server (PLC stop)
+</xsl:text>
+          <xsl:text>    //    - connection was closed locally with an intention to reconnect
+</xsl:text>
+          <xsl:text>    if(evt.code=1000 &amp;&amp; !force_reconnect){
+</xsl:text>
+          <xsl:text>        window.alert("Connection closed by server");
+</xsl:text>
+          <xsl:text>        location.reload();
+</xsl:text>
+          <xsl:text>    }
 </xsl:text>
           <xsl:text>    window.setTimeout(create_ws, reconnect_delay);
 </xsl:text>
           <xsl:text>    reconnect_delay += 500;
 </xsl:text>
+          <xsl:text>    force_reconnect = false;
+</xsl:text>
           <xsl:text>};
 </xsl:text>
           <xsl:text>
--- a/svghmi/svghmi.js	Sun Nov 20 18:36:13 2022 +0100
+++ b/svghmi/svghmi.js	Wed Nov 23 14:18:25 2022 +0100
@@ -525,6 +525,9 @@
         ? page_name
         : page_name + "@" + hmitree_paths[page_index]);
 
+    // when entering a page, assignments are evaluated
+    new_desc.widgets[0][0].assign();
+
     return true;
 };
 
@@ -607,6 +610,7 @@
 
 var reconnect_delay = 0;
 var periodic_reconnect_timer;
+var force_reconnect = false;
 
 // Once connection established
 function ws_onopen(evt) {
@@ -617,6 +621,7 @@
             window.clearTimeout(periodic_reconnect_timer);
         }
         periodic_reconnect_timer = window.setTimeout(() => {
+            force_reconnect = true;
             ws.close();
             periodic_reconnect_timer = null;
         }, 3600000);
@@ -635,10 +640,16 @@
 function ws_onclose(evt) {
     console.log("Connection closed. code:"+evt.code+" reason:"+evt.reason+" wasClean:"+evt.wasClean+" Reload in "+reconnect_delay+"ms.");
     ws = null;
-    // reconect
-    // TODO : add visible notification while waiting for reload
+    // Do not attempt to reconnect immediately in case:
+    //    - connection was closed by server (PLC stop)
+    //    - connection was closed locally with an intention to reconnect
+    if(evt.code=1000 && !force_reconnect){
+        window.alert("Connection closed by server");
+        location.reload();
+    }
     window.setTimeout(create_ws, reconnect_delay);
     reconnect_delay += 500;
+    force_reconnect = false;
 };
 
 var ws_url =
--- a/svghmi/svghmi.py	Sun Nov 20 18:36:13 2022 +0100
+++ b/svghmi/svghmi.py	Wed Nov 23 14:18:25 2022 +0100
@@ -655,8 +655,8 @@
 
 def svghmi_{location}_watchdog_trigger():
     global browser_proc
-    restart_proc = {svghmi_cmds[Watchdog]}
-    waitpid_timeout(restart_proc, "SVGHMI watchdog triggered command")
+    watchdog_proc = {svghmi_cmds[Watchdog]}
+    waitpid_timeout(watchdog_proc, "SVGHMI watchdog triggered command")
     stop_proc = {svghmi_cmds[Stop]}
     waitpid_timeout(stop_proc, "SVGHMI stop command")
     waitpid_timeout(browser_proc, "SVGHMI browser process")
--- a/svghmi/widget_jump.ysl2	Sun Nov 20 18:36:13 2022 +0100
+++ b/svghmi/widget_jump.ysl2	Wed Nov 23 14:18:25 2022 +0100
@@ -143,8 +143,8 @@
             otherwise value "$page_desc/arg[1]/@value";
         }
         const "target_page_path" choose {
-            when "arg" value "$hmi_pages_descs[arg[1]/@value = $target_page_name]/path[1]/@value";
-            otherwise value "$page_desc/path[1]/@value";
+            when "arg" value "$hmi_pages_descs[arg[1]/@value = $target_page_name]/path[not(@assign)]/@value";
+            otherwise value "$page_desc/path[not(@assign)]/@value";
         }
 
         if "not(func:same_class_paths($target_page_path, path[1]/@value))"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/widget_page.ysl2	Wed Nov 23 14:18:25 2022 +0100
@@ -0,0 +1,60 @@
+// widget_page.ysl2
+
+widget_desc("Page") {
+    longdesc
+    ||
+
+    Arguments are either:
+
+    - XXX reference path TODO
+
+    - name=value: setting variable with literal value.
+    - name=other_name: copy variable content into another
+
+    "active"+"inactive" labeled elements can be provided to show feedback when pressed
+
+    Exemples:
+
+    HMI:Page:notify=1@notify=/PLCVAR
+    HMI:Page:ack=2:notify=1@ack=.local_var@notify=/PLCVAR
+
+    ||
+
+    shortdesc > Page 
+
+}
+
+widget_defs("Page") {
+
+    |     assignments: {},
+    |     dispatch: function(value, oldval, varnum) {
+    const "widget", ".";
+    foreach "path" {
+        const "varid","generate-id()";
+        const "varnum","position()-1";
+        if "@assign" foreach "$widget/path[@assign]" if "$varid = generate-id()" {
+    |         if(varnum == «$varnum») this.assignments["«@assign»"] = value;
+        }
+    }
+    |     },
+    |     assign: function() {
+    const "paths","path";
+    foreach "arg[contains(@value,'=')]"{
+        const "name","substring-before(@value,'=')";
+        const "value","substring-after(@value,'=')";
+        const "index" foreach "$paths" if "@assign = $name" value "position()-1";
+        const "isVarName", "regexp:test($value,'^[a-zA-Z_][a-zA-Z0-9_]+$')";
+        choose {
+            when "$isVarName"{
+    |         const «$value» = this.assignments["«$value»"];
+    |         if(«$value» != undefined)
+    |             this.apply_hmi_value(«$index», «$value»);
+            }
+            otherwise {
+    |         this.apply_hmi_value(«$index», «$value»);
+            }
+        }
+    }
+    |     },
+}
+
--- a/svghmi/widgets_common.ysl2	Sun Nov 20 18:36:13 2022 +0100
+++ b/svghmi/widgets_common.ysl2	Wed Nov 23 14:18:25 2022 +0100
@@ -134,7 +134,7 @@
     |   "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$variables»],«$enable_expr»,{
     if "$widget/@enable_expr" {
 
-    |       assignments: [],
+    |       enable_assignments: [],
     |       compute_enable: function(value, oldval, varnum) {
     |         let result = false;
     |         do {
@@ -142,8 +142,8 @@
             const "varid","generate-id()";
             const "varnum","position()-1";
             if "@assign" foreach "$widget/path[@assign]" if "$varid = generate-id()" {
-    |           if(varnum == «$varnum») this.assignments[«position()-1»] = value;
-    |           let «@assign» = this.assignments[«position()-1»];
+    |           if(varnum == «$varnum») this.enable_assignments[«position()-1»] = value;
+    |           let «@assign» = this.enable_assignments[«position()-1»];
     |           if(«@assign» == undefined) break;
             }
         }
@@ -600,12 +600,14 @@
 }
 
 const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id";
+const "page_ids","$parsed_widgets/widget[@type = 'Page']/@id";
 const "hmi_widgets","$hmi_elements[@id = $included_ids]";
+const "page_widgets","$hmi_elements[@id = $page_ids]";
 const "result_widgets","$result_svg_ns//*[@id = $hmi_widgets/@id]";
 
 emit "declarations:hmi-elements" {
     | var hmi_widgets = {
-    apply "$hmi_widgets", mode="hmi_widgets";
+    apply "$hmi_widgets | $page_widgets", mode="hmi_widgets";
     | }
     |
 }