Rewrite DebugVariablePanel
authorLaurent Bessard
Mon, 03 Jun 2013 22:09:01 +0200
changeset 1215 786f2533200a
parent 1214 2ef048b5383c
child 1216 598ff0043ad3
Rewrite DebugVariablePanel
controls/DebugVariablePanel/DebugVariableGraphicPanel.py
controls/DebugVariablePanel/DebugVariableGraphicViewer.py
controls/DebugVariablePanel/DebugVariableItem.py
controls/DebugVariablePanel/DebugVariableTextViewer.py
editors/DebugViewer.py
--- a/controls/DebugVariablePanel/DebugVariableGraphicPanel.py	Mon Jun 03 17:29:03 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableGraphicPanel.py	Mon Jun 03 22:09:01 2013 +0200
@@ -41,21 +41,86 @@
 from DebugVariableTextViewer import DebugVariableTextViewer
 from DebugVariableGraphicViewer import *
 
+MILLISECOND = 1000000       # Number of nanosecond in a millisecond
+SECOND = 1000 * MILLISECOND # Number of nanosecond in a second
+MINUTE = 60 * SECOND        # Number of nanosecond in a minute
+HOUR = 60 * MINUTE          # Number of nanosecond in a hour
+DAY = 24 * HOUR             # Number of nanosecond in a day
+
+# List of values possible for graph range
+# Format is [(time_in_plain_text, value_in_nanosecond),...]
+RANGE_VALUES = \
+    [("%dms" % i, i * MILLISECOND) for i in (10, 20, 50, 100, 200, 500)] + \
+    [("%ds" % i, i * SECOND) for i in (1, 2, 5, 10, 20, 30)] + \
+    [("%dm" % i, i * MINUTE) for i in (1, 2, 5, 10, 20, 30)] + \
+    [("%dh" % i, i * HOUR) for i in (1, 2, 3, 6, 12, 24)]
+
+# Scrollbar increment in pixel
+SCROLLBAR_UNIT = 10
+
+def compute_mask(x, y):
+    return [(xp if xp == yp else "*")
+            for xp, yp in zip(x, y)]
+
+def NextTick(variables):
+    next_tick = None
+    for item, data in variables:
+        if len(data) == 0:
+            continue
+        
+        next = (data[0][0]
+                if next_tick is None
+                else min(next_tick, data[0][0]))
+    
+    return next_tick
+
+#-------------------------------------------------------------------------------
+#                    Debug Variable Graphic Panel Drop Target
+#-------------------------------------------------------------------------------
+
+"""
+Class that implements a custom drop target class for Debug Variable Graphic
+Panel
+"""
+
 class DebugVariableDropTarget(wx.TextDropTarget):
     
-    def __init__(self, parent):
+    def __init__(self, window):
+        """
+        Constructor
+        @param window: Reference to the Debug Variable Panel
+        """
         wx.TextDropTarget.__init__(self)
-        self.ParentWindow = parent
+        self.ParentWindow = window
     
     def __del__(self):
+        """
+        Destructor
+        """
+        # Remove reference to Debug Variable Panel
         self.ParentWindow = None
     
     def OnDragOver(self, x, y, d):
+        """
+        Function called when mouse is dragged over Drop Target
+        @param x: X coordinate of mouse pointer
+        @param y: Y coordinate of mouse pointer
+        @param d: Suggested default for return value
+        """
+       # Signal Debug Variable Panel to refresh highlight giving mouse position
         self.ParentWindow.RefreshHighlight(x, y)
         return wx.TextDropTarget.OnDragOver(self, x, y, d)
         
     def OnDropText(self, x, y, data):
+        """
+        Function called when mouse is released in Drop Target
+        @param x: X coordinate of mouse pointer
+        @param y: Y coordinate of mouse pointer
+        @param data: Text associated to drag'n drop
+        """
         message = None
+        
+        # Check that data is valid regarding DebugVariablePanel
         try:
             values = eval(data)
             if not isinstance(values, TupleType):
@@ -64,80 +129,87 @@
             message = _("Invalid value \"%s\" for debug variable")%data
             values = None
             
+        # Display message if data is invalid
         if message is not None:
             wx.CallAfter(self.ShowMessage, message)
         
+        # Data contain a reference to a variable to debug
         elif values[1] == "debug":
+            
+            # Drag'n Drop is an internal is an internal move inside Debug
+            # Variable Panel 
             if len(values) > 2 and values[2] == "move":
                 self.ParentWindow.MoveValue(values[0])
+            
+            # Drag'n Drop was initiated by another control of Beremiz
             else:
                 self.ParentWindow.InsertValue(values[0], force=True)
     
     def OnLeave(self):
+        """
+        Function called when mouse is leave Drop Target
+        """
+        # Signal Debug Variable Panel to reset highlight
         self.ParentWindow.ResetHighlight()
         return wx.TextDropTarget.OnLeave(self)
     
     def ShowMessage(self, message):
+        """
+        Show error message in Error Dialog
+        @param message: Error message to display
+        """
         dialog = wx.MessageDialog(self.ParentWindow, message, _("Error"), wx.OK|wx.ICON_ERROR)
         dialog.ShowModal()
         dialog.Destroy()
 
-MILLISECOND = 1000000
-SECOND = 1000 * MILLISECOND
-MINUTE = 60 * SECOND
-HOUR = 60 * MINUTE
-DAY = 24 * HOUR
-
-ZOOM_VALUES = map(lambda x:("x %.1f" % x, x), [math.sqrt(2) ** i for i in xrange(8)])
-RANGE_VALUES = \
-    [("%dms" % i, i * MILLISECOND) for i in (10, 20, 50, 100, 200, 500)] + \
-    [("%ds" % i, i * SECOND) for i in (1, 2, 5, 10, 20, 30)] + \
-    [("%dm" % i, i * MINUTE) for i in (1, 2, 5, 10, 20, 30)] + \
-    [("%dh" % i, i * HOUR) for i in (1, 2, 3, 6, 12, 24)]
-
-SCROLLBAR_UNIT = 10
-
-def compute_mask(x, y):
-    mask = []
-    for xp, yp in zip(x, y):
-        if xp == yp:
-            mask.append(xp)
-        else:
-            mask.append("*")
-    return mask
-
-def NextTick(variables):
-    next_tick = None
-    for item, data in variables:
-        if len(data) > 0:
-            if next_tick is None:
-                next_tick = data[0][0]
-            else:
-                next_tick = min(next_tick, data[0][0])
-    return next_tick
+
+#-------------------------------------------------------------------------------
+#                      Debug Variable Graphic Panel Class
+#-------------------------------------------------------------------------------
+
+"""
+Class that implements a Viewer that display variable values as a graphs
+"""
 
 class DebugVariableGraphicPanel(wx.Panel, DebugViewer):
     
     def __init__(self, parent, producer, window):
+        """
+        Constructor
+        @param parent: Reference to the parent wx.Window
+        @param producer: Object receiving debug value and dispatching them to
+        consumers
+        @param window: Reference to Beremiz frame
+        """
         wx.Panel.__init__(self, parent, style=wx.SP_3D|wx.TAB_TRAVERSAL)
         
+        # Save Reference to Beremiz frame
         self.ParentWindow = window
         
+        # Variable storing flag indicating that variable displayed in table
+        # received new value and then table need to be refreshed
         self.HasNewData = False
+        
+        # Variable storing flag indicating that refresh has been forced, and
+        # that next time refresh is possible, it will be done even if no new
+        # data is available
         self.Force = False
         
         self.SetBackgroundColour(wx.WHITE)
         
         main_sizer = wx.BoxSizer(wx.VERTICAL)
         
-        self.Ticks = numpy.array([])
-        self.StartTick = 0
-        self.Fixed = False
-        self.CursorTick = None
+        self.Ticks = numpy.array([]) # List of tick received
+        self.StartTick = 0           # Tick starting range of data displayed
+        self.Fixed = False           # Flag that range of data is fixed
+        self.CursorTick = None       # Tick of cursor for displaying values
+        
+        # 
         self.DraggingAxesPanel = None
         self.DraggingAxesBoundingBox = None
         self.DraggingAxesMousePos = None
         self.VetoScrollEvent = False
+        
         self.VariableNameMask = []
         
         self.GraphicPanels = []
@@ -215,40 +287,78 @@
         
         self.SetSizer(main_sizer)
     
-    def __del__(self):
-        DebugViewer.__del__(self)
-    
     def SetTickTime(self, ticktime=0):
+        """
+        Set Ticktime for calculate data range according to time range selected
+        @param ticktime: Ticktime to apply to range (default: 0)
+        """
+        # Save ticktime
         self.Ticktime = ticktime
+        
+        # Set ticktime to millisecond if undefined
         if self.Ticktime == 0:
             self.Ticktime = MILLISECOND
+        
+        # Calculate range to apply to data
         self.CurrentRange = RANGE_VALUES[
             self.CanvasRange.GetSelection()][1] / self.Ticktime
     
     def SetDataProducer(self, producer):
+        """
+        Set Data Producer
+        @param producer: Data Producer
+        """
         DebugViewer.SetDataProducer(self, producer)
         
+        # Set ticktime if data producer is available
         if self.DataProducer is not None:
             self.SetTickTime(self.DataProducer.GetTicktime())
     
     def RefreshNewData(self, *args, **kwargs):
+        """
+        Called to refresh Panel according to values received by variables
+        Can receive any parameters (not used here)
+        """
+        # Refresh graphs if new data is available or refresh is forced
         if self.HasNewData or self.Force:
             self.HasNewData = False
             self.RefreshView()
+        
         DebugViewer.RefreshNewData(self, *args, **kwargs)
     
     def NewDataAvailable(self, tick, *args, **kwargs):
+        """
+        Called by DataProducer for each tick captured or by panel to refresh
+        graphs
+        @param tick: PLC tick captured
+        All other parameters are passed to refresh function 
+        """
+        # If tick given
         if tick is not None:
+            
+            # Save tick as start tick for range if data is still empty
             if len(self.Ticks) == 0:
                 self.StartTick = tick 
+            
+            # Add tick to list of ticks received
             self.Ticks = numpy.append(self.Ticks, [tick])
+            
+            # Update start tick for range if range follow ticks received
             if not self.Fixed or tick < self.StartTick + self.CurrentRange:
                 self.StartTick = max(self.StartTick, tick - self.CurrentRange)
-            if self.Fixed and self.Ticks[-1] - self.Ticks[0] < self.CurrentRange:
+            
+            # Force refresh if graph is fixed because range of data received
+            # is too small to fill data range selected
+            if self.Fixed and \
+               self.Ticks[-1] - self.Ticks[0] < self.CurrentRange:
                 self.Force = True
+        
         DebugViewer.NewDataAvailable(self, tick, *args, **kwargs)
     
     def ForceRefresh(self):
+        """
+        Called to force refresh of graphs
+        """
         self.Force = True
         wx.CallAfter(self.NewDataAvailable, None, True)
     
@@ -668,6 +778,7 @@
                 panel.SetCanvasSize(source_size.width, source_size.height)
                 if self.CursorTick is not None:
                     panel.SetCursorTick(self.CursorTick)
+            
             else:
                 panel = DebugVariableTextViewer(self.GraphicsWindow, self, [item])
             
@@ -676,9 +787,10 @@
             if source_panel.ItemsIsEmpty():
                 if source_panel.HasCapture():
                     source_panel.ReleaseMouse()
+                if isinstance(source_panel, DebugVariableGraphicViewer):
+                    source_panel.Destroy()
                 self.GraphicPanels.remove(source_panel)
-                source_panel.Destroy()
-            
+                
             self.ResetVariableNameMask()
             self.RefreshGraphicsSizer()
             self.ForceRefresh()
@@ -716,8 +828,9 @@
                     if source_panel.ItemsIsEmpty():
                         if source_panel.HasCapture():
                             source_panel.ReleaseMouse()
+                        if isinstance(source_panel, DebugVariableGraphicViewer):
+                            source_panel.Destroy()
                         self.GraphicPanels.remove(source_panel)
-                        source_panel.Destroy()
             elif (merge_type != graph_type and len(target_panel.Items) == 2):
                 target_panel.RemoveItem(source_item)
             else:
@@ -745,14 +858,16 @@
             
             if item is None:
                 source_panel.ClearItems()
-                source_panel.Destroy()
+                if isinstance(source_panel, DebugVariableGraphicViewer):
+                    source_panel.Destroy()
                 self.GraphicPanels.remove(source_panel)
                 self.ResetVariableNameMask()
                 self.RefreshGraphicsSizer()
             else:
                 source_panel.RemoveItem(item)
                 if source_panel.ItemsIsEmpty():
-                    source_panel.Destroy()
+                    if isinstance(source_panel, DebugVariableGraphicViewer):
+                        source_panel.Destroy()
                     self.GraphicPanels.remove(source_panel)
                     self.ResetVariableNameMask()
                     self.RefreshGraphicsSizer()
--- a/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Mon Jun 03 17:29:03 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Mon Jun 03 22:09:01 2013 +0200
@@ -58,23 +58,13 @@
 # Color for graph cursor
 CURSOR_COLOR = '#800080'
 
-def OrthogonalDataAndRange(item, start_tick, end_tick):
-    data = item.GetData(start_tick, end_tick)
-    min_value, max_value = item.GetValueRange()
-    if min_value is not None and max_value is not None:
-        center = (min_value + max_value) / 2.
-        range = max(1.0, max_value - min_value)
-    else:
-        center = 0.5
-        range = 1.0
-    return data, center - range * 0.55, center + range * 0.55
-
 #-------------------------------------------------------------------------------
 #                   Debug Variable Graphic Viewer Drop Target
 #-------------------------------------------------------------------------------
 
 """
-Class that implements a custom drop target class for Debug Variable Graphic Viewer
+Class that implements a custom drop target class for Debug Variable Graphic
+Viewer
 """
 
 class DebugVariableGraphicDropTarget(wx.TextDropTarget):
@@ -112,7 +102,7 @@
         
     def OnDropText(self, x, y, data):
         """
-        Function called when mouse is dragged over Drop Target
+        Function called when mouse is released in Drop Target
         @param x: X coordinate of mouse pointer
         @param y: Y coordinate of mouse pointer
         @param data: Text associated to drag'n drop
@@ -1095,8 +1085,8 @@
                 start_tick = max(start_tick, min_start_tick)
                 end_tick = max(end_tick, min_start_tick)
                 items = self.ItemsDict.values()
-                x_data, x_min, x_max = OrthogonalDataAndRange(items[0], start_tick, end_tick)
-                y_data, y_min, y_max = OrthogonalDataAndRange(items[1], start_tick, end_tick)
+                x_data, x_min, x_max = items[0].OrthogonalDataAndRange(start_tick, end_tick)
+                y_data, y_min, y_max = items[1].OrthogonalDataAndRange(start_tick, end_tick)
                 if self.CursorTick is not None:
                     x_cursor, x_forced = items[0].GetValue(self.CursorTick, raw=True)
                     y_cursor, y_forced = items[1].GetValue(self.CursorTick, raw=True)
@@ -1133,7 +1123,7 @@
                 else:
                     while len(self.Axes.lines) > 0:
                         self.Axes.lines.pop()
-                    z_data, z_min, z_max = OrthogonalDataAndRange(items[2], start_tick, end_tick)
+                    z_data, z_min, z_max = items[2].OrthogonalDataAndRange(start_tick, end_tick)
                     if self.CursorTick is not None:
                         z_cursor, z_forced = items[2].GetValue(self.CursorTick, raw=True)
                     if x_data is not None and y_data is not None and z_data is not None:
--- a/controls/DebugVariablePanel/DebugVariableItem.py	Mon Jun 03 17:29:03 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableItem.py	Mon Jun 03 22:09:01 2013 +0200
@@ -172,6 +172,25 @@
         """
         return self.MinValue, self.MaxValue
     
+    def OrthogonalDataAndRange(self, start_tick, end_tick):
+        """
+        Return variable value range
+        @param start_tick: Start tick of given range (default None, first data)
+        @param end_tick: end tick of given range (default None, last data)
+        @return: (numpy.array([(tick, value, forced),...]), 
+                  min_value, max_value)
+        """
+        # Calculate min_value and max_value so that range size is greater
+        # than 1.0
+        if self.MinValue is not None and self.MaxValue is not None:
+            center = (self.MinValue + self.MaxValue) / 2.
+            range = max(1.0, self.MaxValue - self.MinValue)
+        else:
+            center = 0.5
+            range = 1.0
+        return (self.GetData(start_tick, end_tick), 
+                center - range * 0.55, center + range * 0.55)
+    
     def ResetData(self):
         """
         Reset data stored when store data option enabled
--- a/controls/DebugVariablePanel/DebugVariableTextViewer.py	Mon Jun 03 17:29:03 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableTextViewer.py	Mon Jun 03 22:09:01 2013 +0200
@@ -73,7 +73,7 @@
         
     def OnDropText(self, x, y, data):
         """
-        Function called when mouse is dragged over Drop Target
+        Function called when mouse is released in Drop Target
         @param x: X coordinate of mouse pointer
         @param y: Y coordinate of mouse pointer
         @param data: Text associated to drag'n drop
--- a/editors/DebugViewer.py	Mon Jun 03 17:29:03 2013 +0200
+++ b/editors/DebugViewer.py	Mon Jun 03 22:09:01 2013 +0200
@@ -248,7 +248,7 @@
     
     def NewDataAvailable(self, tick, *args, **kwargs):
         """
-        Called by DataProducer for each tick captured in 
+        Called by DataProducer for each tick captured
         @param tick: PLC tick captured
         All other parameters are passed to refresh function 
         """