# HG changeset patch # User Laurent Bessard # Date 1358984754 -3600 # Node ID 852af7c6f0ef652d3a721666ff608c4e11603402 # Parent 50a8192fbb23f183eb3eca13b8200a45c5bafae6 Adding support for change DebugVariablePanel 2D graphs layout using drag'n drop diff -r 50a8192fbb23 -r 852af7c6f0ef IDEFrame.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() diff -r 50a8192fbb23 -r 852af7c6f0ef controls/DebugVariablePanel.py --- 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))