Adding support for change DebugVariablePanel 2D graphs layout using drag'n drop
authorLaurent Bessard
Thu, 24 Jan 2013 00:45:54 +0100
changeset 909 852af7c6f0ef
parent 908 50a8192fbb23
child 910 f6d06bdd31e8
Adding support for change DebugVariablePanel 2D graphs layout using drag'n drop
IDEFrame.py
controls/DebugVariablePanel.py
--- a/IDEFrame.py	Thu Jan 24 00:42:59 2013 +0100
+++ b/IDEFrame.py	Thu Jan 24 00:45:54 2013 +0100
@@ -931,8 +931,7 @@
             project_infos["tabs"] = self.SaveTabLayout(self.TabsOpened)
             if self.EnableDebug:
                 project_infos["debug_vars"] = self.DebugVariablePanel.GetDebugVariables()
-                project_infos["debug_axis3D"] = self.DebugVariablePanel.GetAxis3D()
-            
+                
             self.Config.Write("projects", cPickle.dumps(projects))
             self.Config.Flush()
     
@@ -948,9 +947,8 @@
                 
             if self.EnableDebug:
                 try:
-                    axis3D = project.get("debug_axis3D", [])
                     for variable in project.get("debug_vars", []):
-                        self.DebugVariablePanel.InsertValue(variable, force=True, axis3D=variable in axis3D)
+                        self.DebugVariablePanel.InsertValue(variable, force=True)
                 except:
                     self.DebugVariablePanel.ResetGrid()
             
--- a/controls/DebugVariablePanel.py	Thu Jan 24 00:42:59 2013 +0100
+++ b/controls/DebugVariablePanel.py	Thu Jan 24 00:45:54 2013 +0100
@@ -56,16 +56,15 @@
                     [("%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)]
 
+GRAPH_PARALLEL, GRAPH_ORTHOGONAL = range(2)
+
 def AppendMenu(parent, help, id, kind, text):
     parent.Append(help=help, id=id, kind=kind, text=text)
 
 def GetDebugVariablesTableColnames():
     _ = lambda x : x
-    cols = [_("Variable"), _("Value")]
-    if USE_MPL:
-        cols.append(_("3DAxis"))
-    return cols
-
+    return [_("Variable"), _("Value")]
+    
 class VariableTableItem(DebugDataConsumer):
     
     def __init__(self, parent, variable):
@@ -74,8 +73,7 @@
         self.Variable = variable
         self.RefreshVariableType()
         self.Value = ""
-        self.Axis3D = False
-    
+        
     def __del__(self):
         self.Parent = None
     
@@ -165,15 +163,6 @@
             return "%.6g" % self.Value
         return self.Value
 
-    def SetAxis3D(self, axis_3d):
-        if self.IsNumVariable():
-            self.Axis3D = axis_3d
-        
-    def GetAxis3D(self):
-        if self.IsNumVariable():
-            return self.Axis3D
-        return ""
-
     def GetNearestData(self, tick, adjust):
         if self.IsNumVariable():
             ticks = self.Data[:, 0]
@@ -202,8 +191,6 @@
                 return self.data[row].GetVariable()
             elif colname == "Value":
                 return self.data[row].GetValue()
-            elif colname == "3DAxis":
-                return self.data[row].GetAxis3D()
         return ""
 
     def SetValueByName(self, row, colname, value):
@@ -212,8 +199,6 @@
                 self.data[row].SetVariable(value)
             elif colname == "Value":
                 self.data[row].SetValue(value)
-            elif colname == "3DAxis":
-                self.data[row].SetAxis3D(value)
     
     def IsForced(self, row):
         if row < self.GetNumberRows():
@@ -236,20 +221,12 @@
         for row in range(self.GetNumberRows()):
             for col in range(self.GetNumberCols()):
                 colname = self.GetColLabelValue(col, False)
-                if colname == "3DAxis":
-                    if self.IsNumVariable(row):
-                        grid.SetCellRenderer(row, col, wx.grid.GridCellBoolRenderer())
-                        grid.SetCellEditor(row, col, wx.grid.GridCellBoolEditor())
-                        grid.SetReadOnly(row, col, False)
+                if colname == "Value":
+                    if self.IsForced(row):
+                        grid.SetCellTextColour(row, col, wx.BLUE)
                     else:
-                        grid.SetReadOnly(row, col, True)
-                else:
-                    if colname == "Value":
-                        if self.IsForced(row):
-                            grid.SetCellTextColour(row, col, wx.BLUE)
-                        else:
-                            grid.SetCellTextColour(row, col, wx.BLACK)
-                    grid.SetReadOnly(row, col, True)
+                        grid.SetCellTextColour(row, col, wx.BLACK)
+                grid.SetReadOnly(row, col, True)
             self.ResizeRow(grid, row)
                 
     def AppendItem(self, data):
@@ -269,15 +246,12 @@
 
 class DebugVariableDropTarget(wx.TextDropTarget):
     
-    def __init__(self, parent):
+    def __init__(self, parent, control):
         wx.TextDropTarget.__init__(self)
         self.ParentWindow = parent
+        self.ParentControl = control
     
     def OnDropText(self, x, y, data):
-        x, y = self.ParentWindow.VariablesGrid.CalcUnscrolledPosition(x, y)
-        row = self.ParentWindow.VariablesGrid.YToRow(y - self.ParentWindow.VariablesGrid.GetColLabelSize())
-        if row == wx.NOT_FOUND:
-            row = self.ParentWindow.Table.GetNumberRows()
         message = None
         try:
             values = eval(data)
@@ -287,10 +261,33 @@
         if not isinstance(values, TupleType):
             message = _("Invalid value \"%s\" for debug variable")%data
             values = None
-        if values is not None and values[1] == "debug":
-            self.ParentWindow.InsertValue(values[0], row)
+        
         if message is not None:
             wx.CallAfter(self.ShowMessage, message)
+        elif values is not None and values[1] == "debug":
+            if self.ParentControl == self.ParentWindow.VariablesGrid:
+                x, y = self.ParentWindow.VariablesGrid.CalcUnscrolledPosition(x, y)
+                row = self.ParentWindow.VariablesGrid.YToRow(y - self.ParentWindow.VariablesGrid.GetColLabelSize())
+                if row == wx.NOT_FOUND:
+                    row = self.ParentWindow.Table.GetNumberRows()
+                self.ParentWindow.InsertValue(values[0], row, force=True)
+            else:
+                x, y = self.ParentWindow.GraphicsCanvasWindow.CalcUnscrolledPosition(x, y)
+                width, height = self.ParentWindow.GraphicsCanvas.GetSize()
+                target = None
+                merge_type = GRAPH_PARALLEL
+                for infos in self.ParentWindow.GraphicsAxes:
+                    ax, ay, aw, ah = infos["axes"].get_position().bounds
+                    rect = wx.Rect(ax * width, height - (ay + ah) * height,
+                                   aw * width, ah * height)
+                    if rect.InsideXY(x, y):
+                        target = infos
+                        merge_rect = wx.Rect(ax * width, height - (ay + ah) * height,
+                                             aw * width / 2., ah * height)
+                        if merge_rect.InsideXY(x, y):
+                            merge_type = GRAPH_ORTHOGONAL
+                        break
+                self.ParentWindow.MergeGraphs(values[0], target, merge_type, force=True)
             
     def ShowMessage(self, message):
         dialog = wx.MessageDialog(self.ParentWindow, message, _("Error"), wx.OK|wx.ICON_ERROR)
@@ -309,6 +306,17 @@
                 next_tick = min(next_tick, data[0][0])
     return next_tick
 
+def OrthogonalData(item, start_tick, end_tick):
+    data = item.GetData(start_tick, end_tick)
+    min_value, max_value = item.GetRange()
+    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 
+    
 class DebugVariablePanel(wx.SplitterWindow, DebugViewer):
     
     def __init__(self, parent, producer, window):
@@ -343,13 +351,11 @@
             button_sizer.AddWindow(button, border=5, flag=wx.LEFT)
         
         self.VariablesGrid = CustomGrid(self.MainPanel, size=wx.Size(-1, 150), style=wx.VSCROLL)
-        self.VariablesGrid.SetDropTarget(DebugVariableDropTarget(self))
+        self.VariablesGrid.SetDropTarget(DebugVariableDropTarget(self, self.VariablesGrid))
         self.VariablesGrid.Bind(wx.grid.EVT_GRID_CELL_RIGHT_CLICK, 
               self.OnVariablesGridCellRightClick)
         self.VariablesGrid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK, 
               self.OnVariablesGridCellLeftClick)
-        self.VariablesGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, 
-              self.OnVariablesGridCellChange)
         main_panel_sizer.AddWindow(self.VariablesGrid, flag=wx.GROW)
         
         self.MainPanel.SetSizer(main_panel_sizer)
@@ -397,14 +403,13 @@
         
         self.VariablesGrid.SetRowLabelSize(0)
         
+        self.GridColSizes = [200, 100]
+        
         for col in range(self.Table.GetNumberCols()):
             attr = wx.grid.GridCellAttr()
-            if self.Table.GetColLabelValue(col, False) == "3DAxis":
-                attr.SetAlignment(wx.ALIGN_CENTER, wx.ALIGN_CENTER)
-            else:
-                attr.SetAlignment(wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
+            attr.SetAlignment(wx.ALIGN_RIGHT, wx.ALIGN_CENTER)
             self.VariablesGrid.SetColAttr(col, attr)
-            self.VariablesGrid.SetColSize(col, 100)
+            self.VariablesGrid.SetColSize(col, self.GridColSizes[col])
         
         self.Table.ResetView(self.VariablesGrid)
         self.VariablesGrid.RefreshButtons()
@@ -458,10 +463,11 @@
             graphics_canvas_window_sizer = wx.BoxSizer(wx.VERTICAL)
             
             self.GraphicsFigure = matplotlib.figure.Figure()
-            self.GraphicsFigure.subplots_adjust(hspace=0)
             self.GraphicsAxes = []
             
             self.GraphicsCanvas = FigureCanvas(self.GraphicsCanvasWindow, -1, self.GraphicsFigure)
+            self.GraphicsCanvas.mpl_connect("button_press_event", self.OnGraphicsCanvasClick)
+            self.GraphicsCanvas.SetDropTarget(DebugVariableDropTarget(self, self.GraphicsCanvas))
             graphics_canvas_window_sizer.AddWindow(self.GraphicsCanvas, 1, flag=wx.GROW)
             
             self.GraphicsCanvasWindow.SetSizer(graphics_canvas_window_sizer)
@@ -528,56 +534,102 @@
         
         self.Thaw()
         
-        if USE_MPL and (not self.Fixed or self.Force):
-            self.Force = False
-            
-            # Refresh graphics
-            start_tick, end_tick = self.StartTick, self.StartTick + self.CurrentRange
+        if USE_MPL:
+        
+            if not self.Fixed or self.Force:
+                self.Force = False
+                
+                # Refresh graphics
+                start_tick, end_tick = self.StartTick, self.StartTick + self.CurrentRange
+                for infos in self.GraphicsAxes:
+                    
+                    if infos["type"] == GRAPH_PARALLEL:
+                        min_value = max_value = None
+                        
+                        for idx, item in enumerate(infos["items"]):
+                            data = item.GetData(start_tick, end_tick)
+                            if data is not None:
+                                item_min_value, item_max_value = item.GetRange()
+                                if min_value is None:
+                                    min_value = item_min_value
+                                elif item_min_value is not None:
+                                    min_value = min(min_value, item_min_value)
+                                if max_value is None:
+                                    max_value = item_max_value
+                                elif item_max_value is not None:
+                                    max_value = max(max_value, item_max_value)
+                                
+                                if len(infos["plots"]) <= idx:
+                                    infos["plots"].append(
+                                        infos["axes"].plot(data[:, 0], data[:, 1])[0])
+                                else:
+                                    infos["plots"][idx].set_data(data[:, 0], data[:, 1])
+                        
+                        if min_value is not None and max_value is not None:
+                            y_center = (min_value + max_value) / 2.
+                            y_range = max(1.0, max_value - min_value)
+                        else:
+                            y_center = 0.5
+                            y_range = 1.0
+                        x_min, x_max = start_tick, end_tick
+                        y_min, y_max = y_center - y_range * 0.55, y_center + y_range * 0.55
+                    
+                    else:
+                        min_start_tick = reduce(max, [item.GetData()[0, 0] 
+                                                      for item in infos["items"]
+                                                      if len(item.GetData()) > 0], 0)
+                        start_tick = max(self.StartTick, min_start_tick)
+                        end_tick = max(self.StartTick + self.CurrentRange, min_start_tick)
+                        x_data, x_min, x_max = OrthogonalData(infos["items"][0], start_tick, end_tick)
+                        y_data, y_min, y_max = OrthogonalData(infos["items"][1], start_tick, end_tick)
+                        length = 0
+                        if x_data is not None and y_data is not None:  
+                            length = min(len(x_data), len(y_data))
+                        if len(infos["items"]) < 3:
+                            if x_data is not None and y_data is not None:
+                                if len(infos["plots"]) == 0:
+                                    infos["plots"].append(
+                                        infos["axes"].plot(x_data[:, 1][:length], 
+                                                           y_data[:, 1][:length])[0])
+                                else:
+                                    infos["plots"][0].set_data(
+                                        x_data[:, 1][:length], 
+                                        y_data[:, 1][:length])
+                        else:
+                            while len(infos["axes"].lines) > 0:
+                                infos["axes"].lines.pop()
+                            z_data, z_min, z_max = OrthogonalData(infos["items"][2], start_tick, end_tick)
+                            if x_data is not None and y_data is not None and z_data is not None:
+                                length = min(length, len(z_data))
+                                infos["axes"].plot(x_data[:, 1][:length],
+                                                   y_data[:, 1][:length],
+                                                   zs = z_data[:, 1][:length])
+                            infos["axes"].set_zlim(z_min, z_max)
+                    
+                    infos["axes"].set_xlim(x_min, x_max)
+                    infos["axes"].set_ylim(y_min, y_max)
+                
+            plot2d = plot3d = 0
             for infos in self.GraphicsAxes:
-                min_value = max_value = None
-                
-                for idx, item in enumerate(infos["items"]):
-                    data = item.GetData(start_tick, end_tick)
-                    if data is not None:
-                        item_min_value, item_max_value = item.GetRange()
-                        if min_value is None:
-                            min_value = item_min_value
-                        elif item_min_value is not None:
-                            min_value = min(min_value, item_min_value)
-                        if max_value is None:
-                            max_value = item_max_value
-                        elif item_max_value is not None:
-                            max_value = max(max_value, item_max_value)
-                        
-                        if len(infos["plots"]) <= idx:
-                            infos["plots"].append(infos["axes"].plot(data[:, 0], data[:, 1])[0])
-                        else:
-                            infos["plots"][idx].set_data(data[:, 0], data[:, 1])
-                
-                if min_value is not None and max_value is not None:
-                    y_center = (min_value + max_value) / 2.
-                    y_range = max(1.0, max_value - min_value)
+                labels = ["%s: %s" % (item.GetVariable(), item.GetValue())
+                          for item in infos["items"]]
+                if infos["type"] == GRAPH_PARALLEL:
+                    infos["axes"].legend(infos["plots"], labels, 
+                        loc="upper left", frameon=False,
+                        prop={'size':'small'})
+                    plot2d += 1
                 else:
-                    y_center = 0.5
-                    y_range = 1.0
-                infos["axes"].set_xlim(start_tick, end_tick)
-                infos["axes"].set_ylim(y_center - y_range * 0.55, y_center + y_range * 0.55)
-            
-            if len(self.GraphicsAxes) > 0:
+                    infos["axes"].set_xlabel(labels[0], fontdict={'size':'small'})
+                    infos["axes"].set_ylabel(labels[1], fontdict={'size':'small'})
+                    if len(labels) > 2:
+                        infos["axes"].set_zlabel(labels[2], fontdict={'size':'small'})
+                        plot3d += 1
+                    else:
+                        plot2d += 1
+            
+            if plot2d > 0:
                 self.GraphicsCanvas.draw()
-                    
-            # Refresh 3D graphics
-            while len(self.Graphics3DAxes.lines) > 0:
-                self.Graphics3DAxes.lines.pop()
-            if self.Axis3DValues is not None:
-                axis = self.Axis3DValues[0]
-                start_tick = max(self.StartTick, self.Axis3DValues[1])
-                end_tick = max(self.StartTick + self.CurrentRange, self.Axis3DValues[1])
-                xyz_data = [axe.GetData(start_tick, end_tick)[:, 1] for axe in axis]
-                length = reduce(min, [len(data) for data in xyz_data])
-                self.Graphics3DAxes.plot(xyz_data[0][:length],
-                    xyz_data[1][:length],
-                    zs = xyz_data[2][:length])
+            if plot3d > 0:
                 self.Graphics3DCanvas.draw()
             
     def UnregisterObsoleteData(self):
@@ -683,12 +735,6 @@
             menu.Destroy()
         event.Skip()
     
-    def OnVariablesGridCellChange(self, event):
-        row, col = event.GetRow(), event.GetCol()
-        if self.Table.GetColLabelValue(col, False) == "3DAxis":
-            wx.CallAfter(self.Reset3DGraphics)
-        event.Skip()
-    
     def RefreshRange(self):
         if len(self.Ticks) > 0:
             if self.Fixed and self.Ticks[-1] - self.Ticks[0] < self.CurrentRange:
@@ -760,7 +806,7 @@
             wx.CallAfter(self.NewDataAvailable, None, True)
         event.Skip()
     
-    def InsertValue(self, iec_path, idx = None, force=False, axis3D=False):
+    def InsertValue(self, iec_path, idx = None, force=False):
         if idx is None:
             idx = self.Table.GetNumberRows()
         for item in self.Table.GetData():
@@ -774,17 +820,71 @@
                 self.GraphicsAxes.append({
                     "items": [item],
                     "axes": None,
-                    "type": "y",
+                    "type": GRAPH_PARALLEL,
                     "plots": []})
-            item.SetAxis3D(int(axis3D))
             self.ResetGraphics()
             self.RefreshGrid()
+    
+    def MergeGraphs(self, source, target_infos, merge_type, force=False):
+        source_item = None
+        for item in self.Table.GetData():
+            if item.GetVariable() == source:
+                source_item = item
+        if source_item is None:
+            item = VariableTableItem(self, source)
+            if item.IsNumVariable():
+                result = self.AddDataConsumer(source.upper(), item)
+                if result is not None or force:
+                    self.Table.InsertItem(self.Table.GetNumberRows(), item)
+                    source_item = item
+        if source_item is not None:
+            source_infos = None
+            for infos in self.GraphicsAxes:
+                if source_item in infos["items"]:
+                    source_infos = infos
+                    break
+            if target_infos is None and source_infos is None:
+                self.GraphicsAxes.append({
+                    "items": [source_item],
+                    "axes": None,
+                    "type": GRAPH_PARALLEL,
+                    "plots": []})
+                
+                self.ResetGraphics()
+                self.RefreshGrid()
+            
+            elif target_infos is not None:
+                if (merge_type == GRAPH_PARALLEL and target_infos["type"] != merge_type or
+                    merge_type == GRAPH_ORTHOGONAL and 
+                    (target_infos["type"] == GRAPH_PARALLEL and len(target_infos["items"]) > 1 or
+                     target_infos["type"] == GRAPH_ORTHOGONAL and len(target_infos["items"]) >= 3)):
+                    return
+                
+                if source_infos is not None:
+                    source_infos["items"].remove(source_item)
+                    if len(source_infos["items"]) == 0:
+                        self.GraphicsAxes.remove(source_infos)
+                
+                target_infos["items"].append(source_item)
+                target_infos["type"] = merge_type
+                
+                self.ResetGraphics()
+                self.RefreshGrid()
             
     def GetDebugVariables(self):
         return [item.GetVariable() for item in self.Table.GetData()]
     
-    def GetAxis3D(self):
-        return [item.GetVariable() for item in self.Table.GetData() if item.GetAxis3D()]
+    def OnGraphicsCanvasClick(self, event):
+        for infos in self.GraphicsAxes:
+            if infos["axes"] == event.inaxes:
+                if len(infos["items"]) == 1:
+                    data = wx.TextDataObject(str((infos["items"][0].GetVariable(), "debug")))
+                    dragSource = wx.DropSource(self.GraphicsCanvas)
+                    dragSource.SetData(data)
+                    dragSource.DoDragDrop()
+                    if self.GraphicsCanvas.HasCapture():
+                        self.GraphicsCanvas.ReleaseMouse()
+                break
     
     def ResetGraphicsValues(self):
         self.Ticks = numpy.array([])
@@ -796,36 +896,26 @@
         if USE_MPL:
             self.GraphicsFigure.clear()
             
-            axes_num = len(self.GraphicsAxes)
-            for idx in xrange(axes_num):
-                if idx == 0:
-                    axes = self.GraphicsFigure.add_subplot(axes_num, 1, idx + 1)
+            axes_num = 0
+            for infos in self.GraphicsAxes:
+                if infos["type"] != GRAPH_ORTHOGONAL or len(infos["items"]) < 3:
+                    axes_num += 1
+            if axes_num == len(self.GraphicsAxes):
+                self.Graphics3DCanvas.Hide()
+            else:
+                self.Graphics3DCanvas.Show()
+            self.GraphicsPanelSizer.Layout()
+            idx = 1
+            for infos in self.GraphicsAxes:
+                if infos["type"] != GRAPH_ORTHOGONAL or len(infos["items"]) < 3:
+                    axes = self.GraphicsFigure.add_subplot(axes_num, 1, idx)
+                    infos["axes"] = axes
                 else:
-                    axes = self.GraphicsFigure.add_subplot(axes_num, 1, idx + 1, sharex=self.GraphicsAxes[0]["axes"])
-                self.GraphicsAxes[idx]["axes"] = axes
-                self.GraphicsAxes[idx]["plots"] = []
-            
+                    infos["axes"] = self.Graphics3DAxes
+                infos["plots"] = []
+                idx += 1
             self.RefreshGraphicsCanvasWindowScrollbars()
             self.GraphicsCanvas.draw()
-            
-            self.Reset3DGraphics()
-    
-    def Reset3DGraphics(self):
-        self.Axis3DValues = None
-        axis = [item for item in self.Table.GetData() if item.GetAxis3D()]
-        if len(axis) == 3:
-            max_tick = None
-            xaxis, yaxis, zaxis = [item.GetData() for item in axis]
-            if len(xaxis) > 0 and len(yaxis) > 0 and len(zaxis) > 0:
-                max_tick = max(xaxis[0, 0], yaxis[0, 0], zaxis[0, 0])
-            if max_tick is not None:
-                self.Axis3DValues = (axis, max_tick)
-            else:
-                self.Axis3DValues = (axis, 0)
-            self.Graphics3DCanvas.Show()
-        else:
-            self.Graphics3DCanvas.Hide()
-        self.GraphicsPanelSizer.Layout()
     
     def OnGraphics3DMotion(self, event):
         current_time = gettime()
@@ -836,7 +926,7 @@
     def RefreshGraphicsCanvasWindowScrollbars(self):
         xstart, ystart = self.GraphicsCanvasWindow.GetViewStart()
         window_size = self.GraphicsCanvasWindow.GetClientSize()
-        vwidth, vheight = (window_size[0], (len(self.GraphicsAxes) + 1) * 50)
+        vwidth, vheight = (window_size[0], (len(self.GraphicsAxes) + 1) * 100)
         self.GraphicsCanvas.SetMinSize(wx.Size(vwidth, vheight))
         posx = max(0, min(xstart, (vwidth - window_size[0]) / SCROLLBAR_UNIT))
         posy = max(0, min(ystart, (vheight - window_size[1]) / SCROLLBAR_UNIT))