1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 #This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor |
|
5 #based on the plcopen standard. |
|
6 # |
|
7 #Copyright (C) 2012: Edouard TISSERANT and Laurent BESSARD |
|
8 # |
|
9 #See COPYING file for copyrights details. |
|
10 # |
|
11 #This library is free software; you can redistribute it and/or |
|
12 #modify it under the terms of the GNU General Public |
|
13 #License as published by the Free Software Foundation; either |
|
14 #version 2.1 of the License, or (at your option) any later version. |
|
15 # |
|
16 #This library is distributed in the hope that it will be useful, |
|
17 #but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
19 #General Public License for more details. |
|
20 # |
|
21 #You should have received a copy of the GNU General Public |
|
22 #License along with this library; if not, write to the Free Software |
|
23 #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|
24 |
|
25 from types import TupleType |
|
26 import math |
|
27 import numpy |
|
28 |
|
29 import wx |
|
30 import wx.lib.buttons |
|
31 |
|
32 import matplotlib |
|
33 import matplotlib.pyplot |
|
34 from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap |
|
35 |
|
36 from editors.DebugViewer import DebugViewer |
|
37 from util.BitmapLibrary import GetBitmap |
|
38 |
|
39 from DebugVariableItem import DebugVariableItem |
|
40 from DebugVariableTextViewer import DebugVariableTextViewer |
|
41 from DebugVariableGraphicViewer import * |
|
42 |
|
43 MILLISECOND = 1000000 # Number of nanosecond in a millisecond |
|
44 SECOND = 1000 * MILLISECOND # Number of nanosecond in a second |
|
45 MINUTE = 60 * SECOND # Number of nanosecond in a minute |
|
46 HOUR = 60 * MINUTE # Number of nanosecond in a hour |
|
47 DAY = 24 * HOUR # Number of nanosecond in a day |
|
48 |
|
49 # List of values possible for graph range |
|
50 # Format is [(time_in_plain_text, value_in_nanosecond),...] |
|
51 RANGE_VALUES = \ |
|
52 [("%dms" % i, i * MILLISECOND) for i in (10, 20, 50, 100, 200, 500)] + \ |
|
53 [("%ds" % i, i * SECOND) for i in (1, 2, 5, 10, 20, 30)] + \ |
|
54 [("%dm" % i, i * MINUTE) for i in (1, 2, 5, 10, 20, 30)] + \ |
|
55 [("%dh" % i, i * HOUR) for i in (1, 2, 3, 6, 12, 24)] |
|
56 |
|
57 # Scrollbar increment in pixel |
|
58 SCROLLBAR_UNIT = 10 |
|
59 |
|
60 def compute_mask(x, y): |
|
61 return [(xp if xp == yp else "*") |
|
62 for xp, yp in zip(x, y)] |
|
63 |
|
64 def NextTick(variables): |
|
65 next_tick = None |
|
66 for item, data in variables: |
|
67 if len(data) == 0: |
|
68 continue |
|
69 |
|
70 next_tick = (data[0][0] |
|
71 if next_tick is None |
|
72 else min(next_tick, data[0][0])) |
|
73 |
|
74 return next_tick |
|
75 |
|
76 #------------------------------------------------------------------------------- |
|
77 # Debug Variable Graphic Panel Drop Target |
|
78 #------------------------------------------------------------------------------- |
|
79 |
|
80 """ |
|
81 Class that implements a custom drop target class for Debug Variable Graphic |
|
82 Panel |
|
83 """ |
|
84 |
|
85 class DebugVariableDropTarget(wx.TextDropTarget): |
|
86 |
|
87 def __init__(self, window): |
|
88 """ |
|
89 Constructor |
|
90 @param window: Reference to the Debug Variable Panel |
|
91 """ |
|
92 wx.TextDropTarget.__init__(self) |
|
93 self.ParentWindow = window |
|
94 |
|
95 def __del__(self): |
|
96 """ |
|
97 Destructor |
|
98 """ |
|
99 # Remove reference to Debug Variable Panel |
|
100 self.ParentWindow = None |
|
101 |
|
102 def OnDragOver(self, x, y, d): |
|
103 """ |
|
104 Function called when mouse is dragged over Drop Target |
|
105 @param x: X coordinate of mouse pointer |
|
106 @param y: Y coordinate of mouse pointer |
|
107 @param d: Suggested default for return value |
|
108 """ |
|
109 # Signal Debug Variable Panel to refresh highlight giving mouse position |
|
110 self.ParentWindow.RefreshHighlight(x, y) |
|
111 return wx.TextDropTarget.OnDragOver(self, x, y, d) |
|
112 |
|
113 def OnDropText(self, x, y, data): |
|
114 """ |
|
115 Function called when mouse is released in Drop Target |
|
116 @param x: X coordinate of mouse pointer |
|
117 @param y: Y coordinate of mouse pointer |
|
118 @param data: Text associated to drag'n drop |
|
119 """ |
|
120 # Signal Debug Variable Panel to reset highlight |
|
121 self.ParentWindow.ResetHighlight() |
|
122 |
|
123 message = None |
|
124 |
|
125 # Check that data is valid regarding DebugVariablePanel |
|
126 try: |
|
127 values = eval(data) |
|
128 if not isinstance(values, TupleType): |
|
129 raise ValueError |
|
130 except: |
|
131 message = _("Invalid value \"%s\" for debug variable")%data |
|
132 values = None |
|
133 |
|
134 # Display message if data is invalid |
|
135 if message is not None: |
|
136 wx.CallAfter(self.ShowMessage, message) |
|
137 |
|
138 # Data contain a reference to a variable to debug |
|
139 elif values[1] == "debug": |
|
140 |
|
141 # Drag'n Drop is an internal is an internal move inside Debug |
|
142 # Variable Panel |
|
143 if len(values) > 2 and values[2] == "move": |
|
144 self.ParentWindow.MoveValue(values[0]) |
|
145 |
|
146 # Drag'n Drop was initiated by another control of Beremiz |
|
147 else: |
|
148 self.ParentWindow.InsertValue(values[0], force=True) |
|
149 |
|
150 def OnLeave(self): |
|
151 """ |
|
152 Function called when mouse is leave Drop Target |
|
153 """ |
|
154 # Signal Debug Variable Panel to reset highlight |
|
155 self.ParentWindow.ResetHighlight() |
|
156 return wx.TextDropTarget.OnLeave(self) |
|
157 |
|
158 def ShowMessage(self, message): |
|
159 """ |
|
160 Show error message in Error Dialog |
|
161 @param message: Error message to display |
|
162 """ |
|
163 dialog = wx.MessageDialog(self.ParentWindow, |
|
164 message, |
|
165 _("Error"), |
|
166 wx.OK|wx.ICON_ERROR) |
|
167 dialog.ShowModal() |
|
168 dialog.Destroy() |
|
169 |
|
170 |
|
171 #------------------------------------------------------------------------------- |
|
172 # Debug Variable Graphic Panel Class |
|
173 #------------------------------------------------------------------------------- |
|
174 |
|
175 """ |
|
176 Class that implements a Viewer that display variable values as a graphs |
|
177 """ |
|
178 |
|
179 class DebugVariableGraphicPanel(wx.Panel, DebugViewer): |
|
180 |
|
181 def __init__(self, parent, producer, window): |
|
182 """ |
|
183 Constructor |
|
184 @param parent: Reference to the parent wx.Window |
|
185 @param producer: Object receiving debug value and dispatching them to |
|
186 consumers |
|
187 @param window: Reference to Beremiz frame |
|
188 """ |
|
189 wx.Panel.__init__(self, parent, style=wx.SP_3D|wx.TAB_TRAVERSAL) |
|
190 |
|
191 # Save Reference to Beremiz frame |
|
192 self.ParentWindow = window |
|
193 |
|
194 # Variable storing flag indicating that variable displayed in table |
|
195 # received new value and then table need to be refreshed |
|
196 self.HasNewData = False |
|
197 |
|
198 # Variable storing flag indicating that refresh has been forced, and |
|
199 # that next time refresh is possible, it will be done even if no new |
|
200 # data is available |
|
201 self.Force = False |
|
202 |
|
203 self.SetBackgroundColour(wx.WHITE) |
|
204 |
|
205 main_sizer = wx.BoxSizer(wx.VERTICAL) |
|
206 |
|
207 self.Ticks = numpy.array([]) # List of tick received |
|
208 self.StartTick = 0 # Tick starting range of data displayed |
|
209 self.Fixed = False # Flag that range of data is fixed |
|
210 self.CursorTick = None # Tick of cursor for displaying values |
|
211 |
|
212 self.DraggingAxesPanel = None |
|
213 self.DraggingAxesBoundingBox = None |
|
214 self.DraggingAxesMousePos = None |
|
215 self.VetoScrollEvent = False |
|
216 |
|
217 self.VariableNameMask = [] |
|
218 |
|
219 self.GraphicPanels = [] |
|
220 |
|
221 graphics_button_sizer = wx.BoxSizer(wx.HORIZONTAL) |
|
222 main_sizer.AddSizer(graphics_button_sizer, border=5, flag=wx.GROW|wx.ALL) |
|
223 |
|
224 range_label = wx.StaticText(self, label=_('Range:')) |
|
225 graphics_button_sizer.AddWindow(range_label, flag=wx.ALIGN_CENTER_VERTICAL) |
|
226 |
|
227 self.CanvasRange = wx.ComboBox(self, style=wx.CB_READONLY) |
|
228 self.Bind(wx.EVT_COMBOBOX, self.OnRangeChanged, self.CanvasRange) |
|
229 graphics_button_sizer.AddWindow(self.CanvasRange, 1, |
|
230 border=5, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL) |
|
231 |
|
232 self.CanvasRange.Clear() |
|
233 default_range_idx = 0 |
|
234 for idx, (text, value) in enumerate(RANGE_VALUES): |
|
235 self.CanvasRange.Append(text) |
|
236 if text == "1s": |
|
237 default_range_idx = idx |
|
238 self.CanvasRange.SetSelection(default_range_idx) |
|
239 |
|
240 for name, bitmap, help in [ |
|
241 ("CurrentButton", "current", _("Go to current value")), |
|
242 ("ExportGraphButton", "export_graph", _("Export graph values to clipboard"))]: |
|
243 button = wx.lib.buttons.GenBitmapButton(self, |
|
244 bitmap=GetBitmap(bitmap), |
|
245 size=wx.Size(28, 28), style=wx.NO_BORDER) |
|
246 button.SetToolTipString(help) |
|
247 setattr(self, name, button) |
|
248 self.Bind(wx.EVT_BUTTON, getattr(self, "On" + name), button) |
|
249 graphics_button_sizer.AddWindow(button, border=5, flag=wx.LEFT) |
|
250 |
|
251 self.CanvasPosition = wx.ScrollBar(self, |
|
252 size=wx.Size(0, 16), style=wx.SB_HORIZONTAL) |
|
253 self.CanvasPosition.Bind(wx.EVT_SCROLL_THUMBTRACK, |
|
254 self.OnPositionChanging, self.CanvasPosition) |
|
255 self.CanvasPosition.Bind(wx.EVT_SCROLL_LINEUP, |
|
256 self.OnPositionChanging, self.CanvasPosition) |
|
257 self.CanvasPosition.Bind(wx.EVT_SCROLL_LINEDOWN, |
|
258 self.OnPositionChanging, self.CanvasPosition) |
|
259 self.CanvasPosition.Bind(wx.EVT_SCROLL_PAGEUP, |
|
260 self.OnPositionChanging, self.CanvasPosition) |
|
261 self.CanvasPosition.Bind(wx.EVT_SCROLL_PAGEDOWN, |
|
262 self.OnPositionChanging, self.CanvasPosition) |
|
263 main_sizer.AddWindow(self.CanvasPosition, border=5, flag=wx.GROW|wx.LEFT|wx.RIGHT|wx.BOTTOM) |
|
264 |
|
265 self.TickSizer = wx.BoxSizer(wx.HORIZONTAL) |
|
266 main_sizer.AddSizer(self.TickSizer, border=5, flag=wx.ALL|wx.GROW) |
|
267 |
|
268 self.TickLabel = wx.StaticText(self) |
|
269 self.TickSizer.AddWindow(self.TickLabel, border=5, flag=wx.RIGHT) |
|
270 |
|
271 self.MaskLabel = wx.TextCtrl(self, style=wx.TE_READONLY|wx.TE_CENTER|wx.NO_BORDER) |
|
272 self.TickSizer.AddWindow(self.MaskLabel, 1, border=5, flag=wx.RIGHT|wx.GROW) |
|
273 |
|
274 self.TickTimeLabel = wx.StaticText(self) |
|
275 self.TickSizer.AddWindow(self.TickTimeLabel) |
|
276 |
|
277 self.GraphicsWindow = wx.ScrolledWindow(self, style=wx.HSCROLL|wx.VSCROLL) |
|
278 self.GraphicsWindow.SetBackgroundColour(wx.WHITE) |
|
279 self.GraphicsWindow.SetDropTarget(DebugVariableDropTarget(self)) |
|
280 self.GraphicsWindow.Bind(wx.EVT_ERASE_BACKGROUND, self.OnGraphicsWindowEraseBackground) |
|
281 self.GraphicsWindow.Bind(wx.EVT_PAINT, self.OnGraphicsWindowPaint) |
|
282 self.GraphicsWindow.Bind(wx.EVT_SIZE, self.OnGraphicsWindowResize) |
|
283 self.GraphicsWindow.Bind(wx.EVT_MOUSEWHEEL, self.OnGraphicsWindowMouseWheel) |
|
284 |
|
285 main_sizer.AddWindow(self.GraphicsWindow, 1, flag=wx.GROW) |
|
286 |
|
287 self.GraphicsSizer = wx.BoxSizer(wx.VERTICAL) |
|
288 self.GraphicsWindow.SetSizer(self.GraphicsSizer) |
|
289 |
|
290 DebugViewer.__init__(self, producer, True) |
|
291 |
|
292 self.SetSizer(main_sizer) |
|
293 |
|
294 def SetTickTime(self, ticktime=0): |
|
295 """ |
|
296 Set Ticktime for calculate data range according to time range selected |
|
297 @param ticktime: Ticktime to apply to range (default: 0) |
|
298 """ |
|
299 # Save ticktime |
|
300 self.Ticktime = ticktime |
|
301 |
|
302 # Set ticktime to millisecond if undefined |
|
303 if self.Ticktime == 0: |
|
304 self.Ticktime = MILLISECOND |
|
305 |
|
306 # Calculate range to apply to data |
|
307 self.CurrentRange = RANGE_VALUES[ |
|
308 self.CanvasRange.GetSelection()][1] / self.Ticktime |
|
309 |
|
310 def SetDataProducer(self, producer): |
|
311 """ |
|
312 Set Data Producer |
|
313 @param producer: Data Producer |
|
314 """ |
|
315 DebugViewer.SetDataProducer(self, producer) |
|
316 |
|
317 # Set ticktime if data producer is available |
|
318 if self.DataProducer is not None: |
|
319 self.SetTickTime(self.DataProducer.GetTicktime()) |
|
320 |
|
321 def RefreshNewData(self, *args, **kwargs): |
|
322 """ |
|
323 Called to refresh Panel according to values received by variables |
|
324 Can receive any parameters (not used here) |
|
325 """ |
|
326 # Refresh graphs if new data is available or refresh is forced |
|
327 if self.HasNewData or self.Force: |
|
328 self.HasNewData = False |
|
329 self.RefreshView() |
|
330 |
|
331 DebugViewer.RefreshNewData(self, *args, **kwargs) |
|
332 |
|
333 def NewDataAvailable(self, ticks, *args, **kwargs): |
|
334 """ |
|
335 Called by DataProducer for each tick captured or by panel to refresh |
|
336 graphs |
|
337 @param tick: PLC tick captured |
|
338 All other parameters are passed to refresh function |
|
339 """ |
|
340 # If tick given |
|
341 if ticks is not None: |
|
342 tick = ticks[-1] |
|
343 |
|
344 # Save tick as start tick for range if data is still empty |
|
345 if len(self.Ticks) == 0: |
|
346 self.StartTick = ticks[0] |
|
347 |
|
348 # Add tick to list of ticks received |
|
349 self.Ticks = numpy.append(self.Ticks, ticks) |
|
350 |
|
351 # Update start tick for range if range follow ticks received |
|
352 if not self.Fixed or tick < self.StartTick + self.CurrentRange: |
|
353 self.StartTick = max(self.StartTick, tick - self.CurrentRange) |
|
354 |
|
355 # Force refresh if graph is fixed because range of data received |
|
356 # is too small to fill data range selected |
|
357 if self.Fixed and \ |
|
358 self.Ticks[-1] - self.Ticks[0] < self.CurrentRange: |
|
359 self.Force = True |
|
360 |
|
361 self.HasNewData = False |
|
362 self.RefreshView() |
|
363 |
|
364 else: |
|
365 DebugViewer.NewDataAvailable(self, ticks, *args, **kwargs) |
|
366 |
|
367 def ForceRefresh(self): |
|
368 """ |
|
369 Called to force refresh of graphs |
|
370 """ |
|
371 self.Force = True |
|
372 wx.CallAfter(self.NewDataAvailable, None, True) |
|
373 |
|
374 def SetCursorTick(self, cursor_tick): |
|
375 """ |
|
376 Set Cursor for displaying values of items at a tick given |
|
377 @param cursor_tick: Tick of cursor |
|
378 """ |
|
379 # Save cursor tick |
|
380 self.CursorTick = cursor_tick |
|
381 self.Fixed = cursor_tick is not None |
|
382 self.UpdateCursorTick() |
|
383 |
|
384 def MoveCursorTick(self, move): |
|
385 if self.CursorTick is not None: |
|
386 cursor_tick = max(self.Ticks[0], |
|
387 min(self.CursorTick + move, |
|
388 self.Ticks[-1])) |
|
389 cursor_tick_idx = numpy.argmin(numpy.abs(self.Ticks - cursor_tick)) |
|
390 if self.Ticks[cursor_tick_idx] == self.CursorTick: |
|
391 cursor_tick_idx = max(0, |
|
392 min(cursor_tick_idx + abs(move) / move, |
|
393 len(self.Ticks) - 1)) |
|
394 self.CursorTick = self.Ticks[cursor_tick_idx] |
|
395 self.StartTick = max(self.Ticks[ |
|
396 numpy.argmin(numpy.abs(self.Ticks - |
|
397 self.CursorTick + self.CurrentRange))], |
|
398 min(self.StartTick, self.CursorTick)) |
|
399 self.RefreshCanvasPosition() |
|
400 self.UpdateCursorTick() |
|
401 |
|
402 def ResetCursorTick(self): |
|
403 self.CursorTick = None |
|
404 self.Fixed = False |
|
405 self.UpdateCursorTick() |
|
406 |
|
407 def UpdateCursorTick(self): |
|
408 for panel in self.GraphicPanels: |
|
409 if isinstance(panel, DebugVariableGraphicViewer): |
|
410 panel.SetCursorTick(self.CursorTick) |
|
411 self.ForceRefresh() |
|
412 |
|
413 def StartDragNDrop(self, panel, item, x_mouse, y_mouse, x_mouse_start, y_mouse_start): |
|
414 if len(panel.GetItems()) > 1: |
|
415 self.DraggingAxesPanel = DebugVariableGraphicViewer(self.GraphicsWindow, self, [item], GRAPH_PARALLEL) |
|
416 self.DraggingAxesPanel.SetCursorTick(self.CursorTick) |
|
417 width, height = panel.GetSize() |
|
418 self.DraggingAxesPanel.SetSize(wx.Size(width, height)) |
|
419 self.DraggingAxesPanel.ResetGraphics() |
|
420 self.DraggingAxesPanel.SetPosition(wx.Point(0, -height)) |
|
421 else: |
|
422 self.DraggingAxesPanel = panel |
|
423 self.DraggingAxesBoundingBox = panel.GetAxesBoundingBox(parent_coordinate=True) |
|
424 self.DraggingAxesMousePos = wx.Point( |
|
425 x_mouse_start - self.DraggingAxesBoundingBox.x, |
|
426 y_mouse_start - self.DraggingAxesBoundingBox.y) |
|
427 self.MoveDragNDrop(x_mouse, y_mouse) |
|
428 |
|
429 def MoveDragNDrop(self, x_mouse, y_mouse): |
|
430 self.DraggingAxesBoundingBox.x = x_mouse - self.DraggingAxesMousePos.x |
|
431 self.DraggingAxesBoundingBox.y = y_mouse - self.DraggingAxesMousePos.y |
|
432 self.RefreshHighlight(x_mouse, y_mouse) |
|
433 |
|
434 def RefreshHighlight(self, x_mouse, y_mouse): |
|
435 for idx, panel in enumerate(self.GraphicPanels): |
|
436 x, y = panel.GetPosition() |
|
437 width, height = panel.GetSize() |
|
438 rect = wx.Rect(x, y, width, height) |
|
439 if (rect.InsideXY(x_mouse, y_mouse) or |
|
440 idx == 0 and y_mouse < 0 or |
|
441 idx == len(self.GraphicPanels) - 1 and y_mouse > panel.GetPosition()[1]): |
|
442 panel.RefreshHighlight(x_mouse - x, y_mouse - y) |
|
443 else: |
|
444 panel.SetHighlight(HIGHLIGHT_NONE) |
|
445 if wx.Platform == "__WXMSW__": |
|
446 self.RefreshView() |
|
447 else: |
|
448 self.ForceRefresh() |
|
449 |
|
450 def ResetHighlight(self): |
|
451 for panel in self.GraphicPanels: |
|
452 panel.SetHighlight(HIGHLIGHT_NONE) |
|
453 if wx.Platform == "__WXMSW__": |
|
454 self.RefreshView() |
|
455 else: |
|
456 self.ForceRefresh() |
|
457 |
|
458 def IsDragging(self): |
|
459 return self.DraggingAxesPanel is not None |
|
460 |
|
461 def GetDraggingAxesClippingRegion(self, panel): |
|
462 x, y = panel.GetPosition() |
|
463 width, height = panel.GetSize() |
|
464 bbox = wx.Rect(x, y, width, height) |
|
465 bbox = bbox.Intersect(self.DraggingAxesBoundingBox) |
|
466 bbox.x -= x |
|
467 bbox.y -= y |
|
468 return bbox |
|
469 |
|
470 def GetDraggingAxesPosition(self, panel): |
|
471 x, y = panel.GetPosition() |
|
472 return wx.Point(self.DraggingAxesBoundingBox.x - x, |
|
473 self.DraggingAxesBoundingBox.y - y) |
|
474 |
|
475 def StopDragNDrop(self, variable, x_mouse, y_mouse): |
|
476 if self.DraggingAxesPanel not in self.GraphicPanels: |
|
477 self.DraggingAxesPanel.Destroy() |
|
478 self.DraggingAxesPanel = None |
|
479 self.DraggingAxesBoundingBox = None |
|
480 self.DraggingAxesMousePos = None |
|
481 for idx, panel in enumerate(self.GraphicPanels): |
|
482 panel.SetHighlight(HIGHLIGHT_NONE) |
|
483 xw, yw = panel.GetPosition() |
|
484 width, height = panel.GetSize() |
|
485 bbox = wx.Rect(xw, yw, width, height) |
|
486 if bbox.InsideXY(x_mouse, y_mouse): |
|
487 panel.ShowButtons(True) |
|
488 merge_type = GRAPH_PARALLEL |
|
489 if isinstance(panel, DebugVariableTextViewer) or panel.Is3DCanvas(): |
|
490 if y_mouse > yw + height / 2: |
|
491 idx += 1 |
|
492 wx.CallAfter(self.MoveValue, variable, idx, True) |
|
493 else: |
|
494 rect = panel.GetAxesBoundingBox(True) |
|
495 if rect.InsideXY(x_mouse, y_mouse): |
|
496 merge_rect = wx.Rect(rect.x, rect.y, rect.width / 2., rect.height) |
|
497 if merge_rect.InsideXY(x_mouse, y_mouse): |
|
498 merge_type = GRAPH_ORTHOGONAL |
|
499 wx.CallAfter(self.MergeGraphs, variable, idx, merge_type, force=True) |
|
500 else: |
|
501 if y_mouse > yw + height / 2: |
|
502 idx += 1 |
|
503 wx.CallAfter(self.MoveValue, variable, idx, True) |
|
504 self.ForceRefresh() |
|
505 return |
|
506 width, height = self.GraphicsWindow.GetVirtualSize() |
|
507 rect = wx.Rect(0, 0, width, height) |
|
508 if rect.InsideXY(x_mouse, y_mouse): |
|
509 wx.CallAfter(self.MoveValue, variable, len(self.GraphicPanels), True) |
|
510 self.ForceRefresh() |
|
511 |
|
512 def RefreshGraphicsSizer(self): |
|
513 self.GraphicsSizer.Clear() |
|
514 |
|
515 for panel in self.GraphicPanels: |
|
516 self.GraphicsSizer.AddWindow(panel, flag=wx.GROW) |
|
517 |
|
518 self.GraphicsSizer.Layout() |
|
519 self.RefreshGraphicsWindowScrollbars() |
|
520 |
|
521 def RefreshView(self): |
|
522 self.RefreshCanvasPosition() |
|
523 |
|
524 width, height = self.GraphicsWindow.GetVirtualSize() |
|
525 bitmap = wx.EmptyBitmap(width, height) |
|
526 dc = wx.BufferedDC(wx.ClientDC(self.GraphicsWindow), bitmap) |
|
527 dc.Clear() |
|
528 dc.BeginDrawing() |
|
529 if self.DraggingAxesPanel is not None: |
|
530 destBBox = self.DraggingAxesBoundingBox |
|
531 srcBBox = self.DraggingAxesPanel.GetAxesBoundingBox() |
|
532 |
|
533 srcBmp = _convert_agg_to_wx_bitmap(self.DraggingAxesPanel.get_renderer(), None) |
|
534 srcDC = wx.MemoryDC() |
|
535 srcDC.SelectObject(srcBmp) |
|
536 |
|
537 dc.Blit(destBBox.x, destBBox.y, |
|
538 int(destBBox.width), int(destBBox.height), |
|
539 srcDC, srcBBox.x, srcBBox.y) |
|
540 dc.EndDrawing() |
|
541 |
|
542 if not self.Fixed or self.Force: |
|
543 self.Force = False |
|
544 refresh_graphics = True |
|
545 else: |
|
546 refresh_graphics = False |
|
547 |
|
548 if self.DraggingAxesPanel is not None and self.DraggingAxesPanel not in self.GraphicPanels: |
|
549 self.DraggingAxesPanel.RefreshViewer(refresh_graphics) |
|
550 for panel in self.GraphicPanels: |
|
551 if isinstance(panel, DebugVariableGraphicViewer): |
|
552 panel.RefreshViewer(refresh_graphics) |
|
553 else: |
|
554 panel.RefreshViewer() |
|
555 |
|
556 if self.CursorTick is not None: |
|
557 tick = self.CursorTick |
|
558 elif len(self.Ticks) > 0: |
|
559 tick = self.Ticks[-1] |
|
560 else: |
|
561 tick = None |
|
562 if tick is not None: |
|
563 self.TickLabel.SetLabel("Tick: %d" % tick) |
|
564 tick_duration = int(tick * self.Ticktime) |
|
565 not_null = False |
|
566 duration = "" |
|
567 for value, format in [(tick_duration / DAY, "%dd"), |
|
568 ((tick_duration % DAY) / HOUR, "%dh"), |
|
569 ((tick_duration % HOUR) / MINUTE, "%dm"), |
|
570 ((tick_duration % MINUTE) / SECOND, "%ds")]: |
|
571 |
|
572 if value > 0 or not_null: |
|
573 duration += format % value |
|
574 not_null = True |
|
575 |
|
576 duration += "%gms" % (float(tick_duration % SECOND) / MILLISECOND) |
|
577 self.TickTimeLabel.SetLabel("t: %s" % duration) |
|
578 else: |
|
579 self.TickLabel.SetLabel("") |
|
580 self.TickTimeLabel.SetLabel("") |
|
581 self.TickSizer.Layout() |
|
582 |
|
583 def SubscribeAllDataConsumers(self): |
|
584 DebugViewer.SubscribeAllDataConsumers(self) |
|
585 |
|
586 if self.DataProducer is not None: |
|
587 if self.DataProducer is not None: |
|
588 self.SetTickTime(self.DataProducer.GetTicktime()) |
|
589 |
|
590 self.ResetCursorTick() |
|
591 |
|
592 for panel in self.GraphicPanels[:]: |
|
593 panel.SubscribeAllDataConsumers() |
|
594 if panel.ItemsIsEmpty(): |
|
595 if panel.HasCapture(): |
|
596 panel.ReleaseMouse() |
|
597 self.GraphicPanels.remove(panel) |
|
598 panel.Destroy() |
|
599 |
|
600 self.ResetVariableNameMask() |
|
601 self.RefreshGraphicsSizer() |
|
602 self.ForceRefresh() |
|
603 |
|
604 def ResetView(self): |
|
605 self.UnsubscribeAllDataConsumers() |
|
606 |
|
607 self.Fixed = False |
|
608 for panel in self.GraphicPanels: |
|
609 panel.Destroy() |
|
610 self.GraphicPanels = [] |
|
611 self.ResetVariableNameMask() |
|
612 self.RefreshGraphicsSizer() |
|
613 |
|
614 def SetCanvasPosition(self, tick): |
|
615 tick = max(self.Ticks[0], min(tick, self.Ticks[-1] - self.CurrentRange)) |
|
616 self.StartTick = self.Ticks[numpy.argmin(numpy.abs(self.Ticks - tick))] |
|
617 self.Fixed = True |
|
618 self.RefreshCanvasPosition() |
|
619 self.ForceRefresh() |
|
620 |
|
621 def RefreshCanvasPosition(self): |
|
622 if len(self.Ticks) > 0: |
|
623 pos = int(self.StartTick - self.Ticks[0]) |
|
624 range = int(self.Ticks[-1] - self.Ticks[0]) |
|
625 else: |
|
626 pos = 0 |
|
627 range = 0 |
|
628 self.CanvasPosition.SetScrollbar(pos, self.CurrentRange, range, self.CurrentRange) |
|
629 |
|
630 def ChangeRange(self, dir, tick=None): |
|
631 current_range = self.CurrentRange |
|
632 current_range_idx = self.CanvasRange.GetSelection() |
|
633 new_range_idx = max(0, min(current_range_idx + dir, len(RANGE_VALUES) - 1)) |
|
634 if new_range_idx != current_range_idx: |
|
635 self.CanvasRange.SetSelection(new_range_idx) |
|
636 self.CurrentRange = RANGE_VALUES[new_range_idx][1] / self.Ticktime |
|
637 if len(self.Ticks) > 0: |
|
638 if tick is None: |
|
639 tick = self.StartTick + self.CurrentRange / 2. |
|
640 new_start_tick = min(tick - (tick - self.StartTick) * self.CurrentRange / current_range, |
|
641 self.Ticks[-1] - self.CurrentRange) |
|
642 self.StartTick = self.Ticks[numpy.argmin(numpy.abs(self.Ticks - new_start_tick))] |
|
643 self.Fixed = new_start_tick < self.Ticks[-1] - self.CurrentRange |
|
644 self.ForceRefresh() |
|
645 |
|
646 def RefreshRange(self): |
|
647 if len(self.Ticks) > 0: |
|
648 if self.Fixed and self.Ticks[-1] - self.Ticks[0] < self.CurrentRange: |
|
649 self.Fixed = False |
|
650 if self.Fixed: |
|
651 self.StartTick = min(self.StartTick, self.Ticks[-1] - self.CurrentRange) |
|
652 else: |
|
653 self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange) |
|
654 self.ForceRefresh() |
|
655 |
|
656 def OnRangeChanged(self, event): |
|
657 try: |
|
658 self.CurrentRange = RANGE_VALUES[self.CanvasRange.GetSelection()][1] / self.Ticktime |
|
659 except ValueError, e: |
|
660 self.CanvasRange.SetValue(str(self.CurrentRange)) |
|
661 wx.CallAfter(self.RefreshRange) |
|
662 event.Skip() |
|
663 |
|
664 def OnCurrentButton(self, event): |
|
665 if len(self.Ticks) > 0: |
|
666 self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange) |
|
667 self.ResetCursorTick() |
|
668 event.Skip() |
|
669 |
|
670 def CopyDataToClipboard(self, variables): |
|
671 text = "tick;%s;\n" % ";".join([item.GetVariable() for item, data in variables]) |
|
672 next_tick = NextTick(variables) |
|
673 while next_tick is not None: |
|
674 values = [] |
|
675 for item, data in variables: |
|
676 if len(data) > 0: |
|
677 if next_tick == data[0][0]: |
|
678 var_type = item.GetVariableType() |
|
679 if var_type in ["STRING", "WSTRING"]: |
|
680 value = item.GetRawValue(int(data.pop(0)[2])) |
|
681 if var_type == "STRING": |
|
682 values.append("'%s'" % value) |
|
683 else: |
|
684 values.append('"%s"' % value) |
|
685 else: |
|
686 values.append("%.3f" % data.pop(0)[1]) |
|
687 else: |
|
688 values.append("") |
|
689 else: |
|
690 values.append("") |
|
691 text += "%d;%s;\n" % (next_tick, ";".join(values)) |
|
692 next_tick = NextTick(variables) |
|
693 self.ParentWindow.SetCopyBuffer(text) |
|
694 |
|
695 def OnExportGraphButton(self, event): |
|
696 items = reduce(lambda x, y: x + y, |
|
697 [panel.GetItems() for panel in self.GraphicPanels], |
|
698 []) |
|
699 variables = [(item, [entry for entry in item.GetData()]) |
|
700 for item in items |
|
701 if item.IsNumVariable()] |
|
702 wx.CallAfter(self.CopyDataToClipboard, variables) |
|
703 event.Skip() |
|
704 |
|
705 def OnPositionChanging(self, event): |
|
706 if len(self.Ticks) > 0: |
|
707 self.StartTick = self.Ticks[0] + event.GetPosition() |
|
708 self.Fixed = True |
|
709 self.ForceRefresh() |
|
710 event.Skip() |
|
711 |
|
712 def GetRange(self): |
|
713 return self.StartTick, self.StartTick + self.CurrentRange |
|
714 |
|
715 def GetViewerIndex(self, viewer): |
|
716 if viewer in self.GraphicPanels: |
|
717 return self.GraphicPanels.index(viewer) |
|
718 return None |
|
719 |
|
720 def IsViewerFirst(self, viewer): |
|
721 return viewer == self.GraphicPanels[0] |
|
722 |
|
723 def HighlightPreviousViewer(self, viewer): |
|
724 if self.IsViewerFirst(viewer): |
|
725 return |
|
726 idx = self.GetViewerIndex(viewer) |
|
727 if idx is None: |
|
728 return |
|
729 self.GraphicPanels[idx-1].SetHighlight(HIGHLIGHT_AFTER) |
|
730 |
|
731 def ResetVariableNameMask(self): |
|
732 items = [] |
|
733 for panel in self.GraphicPanels: |
|
734 items.extend(panel.GetItems()) |
|
735 if len(items) > 1: |
|
736 self.VariableNameMask = reduce(compute_mask, |
|
737 [item.GetVariable().split('.') for item in items]) |
|
738 elif len(items) > 0: |
|
739 self.VariableNameMask = items[0].GetVariable().split('.')[:-1] + ['*'] |
|
740 else: |
|
741 self.VariableNameMask = [] |
|
742 self.MaskLabel.ChangeValue(".".join(self.VariableNameMask)) |
|
743 self.MaskLabel.SetInsertionPoint(self.MaskLabel.GetLastPosition()) |
|
744 |
|
745 def GetVariableNameMask(self): |
|
746 return self.VariableNameMask |
|
747 |
|
748 def InsertValue(self, iec_path, idx = None, force=False, graph=False): |
|
749 for panel in self.GraphicPanels: |
|
750 if panel.GetItem(iec_path) is not None: |
|
751 if graph and isinstance(panel, DebugVariableTextViewer): |
|
752 self.ToggleViewerType(panel) |
|
753 return |
|
754 if idx is None: |
|
755 idx = len(self.GraphicPanels) |
|
756 item = DebugVariableItem(self, iec_path, True) |
|
757 result = self.AddDataConsumer(iec_path.upper(), item) |
|
758 if result is not None or force: |
|
759 |
|
760 self.Freeze() |
|
761 if item.IsNumVariable() and graph: |
|
762 panel = DebugVariableGraphicViewer(self.GraphicsWindow, self, [item], GRAPH_PARALLEL) |
|
763 if self.CursorTick is not None: |
|
764 panel.SetCursorTick(self.CursorTick) |
|
765 else: |
|
766 panel = DebugVariableTextViewer(self.GraphicsWindow, self, [item]) |
|
767 if idx is not None: |
|
768 self.GraphicPanels.insert(idx, panel) |
|
769 else: |
|
770 self.GraphicPanels.append(panel) |
|
771 self.ResetVariableNameMask() |
|
772 self.RefreshGraphicsSizer() |
|
773 self.Thaw() |
|
774 self.ForceRefresh() |
|
775 |
|
776 def MoveValue(self, iec_path, idx = None, graph=False): |
|
777 if idx is None: |
|
778 idx = len(self.GraphicPanels) |
|
779 source_panel = None |
|
780 item = None |
|
781 for panel in self.GraphicPanels: |
|
782 item = panel.GetItem(iec_path) |
|
783 if item is not None: |
|
784 source_panel = panel |
|
785 break |
|
786 if source_panel is not None: |
|
787 source_panel_idx = self.GraphicPanels.index(source_panel) |
|
788 |
|
789 if (len(source_panel.GetItems()) == 1): |
|
790 |
|
791 if source_panel_idx < idx: |
|
792 self.GraphicPanels.insert(idx, source_panel) |
|
793 self.GraphicPanels.pop(source_panel_idx) |
|
794 elif source_panel_idx > idx: |
|
795 self.GraphicPanels.pop(source_panel_idx) |
|
796 self.GraphicPanels.insert(idx, source_panel) |
|
797 else: |
|
798 return |
|
799 |
|
800 else: |
|
801 source_panel.RemoveItem(item) |
|
802 source_size = source_panel.GetSize() |
|
803 if item.IsNumVariable() and graph: |
|
804 panel = DebugVariableGraphicViewer(self.GraphicsWindow, self, [item], GRAPH_PARALLEL) |
|
805 panel.SetCanvasHeight(source_size.height) |
|
806 if self.CursorTick is not None: |
|
807 panel.SetCursorTick(self.CursorTick) |
|
808 |
|
809 else: |
|
810 panel = DebugVariableTextViewer(self.GraphicsWindow, self, [item]) |
|
811 |
|
812 self.GraphicPanels.insert(idx, panel) |
|
813 |
|
814 if source_panel.ItemsIsEmpty(): |
|
815 if source_panel.HasCapture(): |
|
816 source_panel.ReleaseMouse() |
|
817 source_panel.Destroy() |
|
818 self.GraphicPanels.remove(source_panel) |
|
819 |
|
820 self.ResetVariableNameMask() |
|
821 self.RefreshGraphicsSizer() |
|
822 self.ForceRefresh() |
|
823 |
|
824 def MergeGraphs(self, source, target_idx, merge_type, force=False): |
|
825 source_item = None |
|
826 source_panel = None |
|
827 for panel in self.GraphicPanels: |
|
828 source_item = panel.GetItem(source) |
|
829 if source_item is not None: |
|
830 source_panel = panel |
|
831 break |
|
832 if source_item is None: |
|
833 item = DebugVariableItem(self, source, True) |
|
834 if item.IsNumVariable(): |
|
835 result = self.AddDataConsumer(source.upper(), item) |
|
836 if result is not None or force: |
|
837 source_item = item |
|
838 if source_item is not None and source_item.IsNumVariable(): |
|
839 if source_panel is not None: |
|
840 source_size = source_panel.GetSize() |
|
841 else: |
|
842 source_size = None |
|
843 target_panel = self.GraphicPanels[target_idx] |
|
844 graph_type = target_panel.GraphType |
|
845 if target_panel != source_panel: |
|
846 if (merge_type == GRAPH_PARALLEL and graph_type != merge_type or |
|
847 merge_type == GRAPH_ORTHOGONAL and |
|
848 (graph_type == GRAPH_PARALLEL and len(target_panel.Items) > 1 or |
|
849 graph_type == GRAPH_ORTHOGONAL and len(target_panel.Items) >= 3)): |
|
850 return |
|
851 |
|
852 if source_panel is not None: |
|
853 source_panel.RemoveItem(source_item) |
|
854 if source_panel.ItemsIsEmpty(): |
|
855 if source_panel.HasCapture(): |
|
856 source_panel.ReleaseMouse() |
|
857 source_panel.Destroy() |
|
858 self.GraphicPanels.remove(source_panel) |
|
859 elif (merge_type != graph_type and len(target_panel.Items) == 2): |
|
860 target_panel.RemoveItem(source_item) |
|
861 else: |
|
862 target_panel = None |
|
863 |
|
864 if target_panel is not None: |
|
865 target_panel.AddItem(source_item) |
|
866 target_panel.GraphType = merge_type |
|
867 size = target_panel.GetSize() |
|
868 if merge_type == GRAPH_ORTHOGONAL: |
|
869 target_panel.SetCanvasHeight(size.width) |
|
870 elif source_size is not None and source_panel != target_panel: |
|
871 target_panel.SetCanvasHeight(size.height + source_size.height) |
|
872 else: |
|
873 target_panel.SetCanvasHeight(size.height) |
|
874 target_panel.ResetGraphics() |
|
875 |
|
876 self.ResetVariableNameMask() |
|
877 self.RefreshGraphicsSizer() |
|
878 self.ForceRefresh() |
|
879 |
|
880 def DeleteValue(self, source_panel, item=None): |
|
881 source_idx = self.GetViewerIndex(source_panel) |
|
882 if source_idx is not None: |
|
883 |
|
884 if item is None: |
|
885 source_panel.ClearItems() |
|
886 source_panel.Destroy() |
|
887 self.GraphicPanels.remove(source_panel) |
|
888 self.ResetVariableNameMask() |
|
889 self.RefreshGraphicsSizer() |
|
890 else: |
|
891 source_panel.RemoveItem(item) |
|
892 if source_panel.ItemsIsEmpty(): |
|
893 source_panel.Destroy() |
|
894 self.GraphicPanels.remove(source_panel) |
|
895 self.ResetVariableNameMask() |
|
896 self.RefreshGraphicsSizer() |
|
897 if len(self.GraphicPanels) == 0: |
|
898 self.Fixed = False |
|
899 self.ResetCursorTick() |
|
900 self.ForceRefresh() |
|
901 |
|
902 def ToggleViewerType(self, panel): |
|
903 panel_idx = self.GetViewerIndex(panel) |
|
904 if panel_idx is not None: |
|
905 self.GraphicPanels.remove(panel) |
|
906 items = panel.GetItems() |
|
907 if isinstance(panel, DebugVariableGraphicViewer): |
|
908 for idx, item in enumerate(items): |
|
909 new_panel = DebugVariableTextViewer(self.GraphicsWindow, self, [item]) |
|
910 self.GraphicPanels.insert(panel_idx + idx, new_panel) |
|
911 else: |
|
912 new_panel = DebugVariableGraphicViewer(self.GraphicsWindow, self, items, GRAPH_PARALLEL) |
|
913 self.GraphicPanels.insert(panel_idx, new_panel) |
|
914 panel.Destroy() |
|
915 self.RefreshGraphicsSizer() |
|
916 self.ForceRefresh() |
|
917 |
|
918 def ResetGraphicsValues(self): |
|
919 self.Ticks = numpy.array([]) |
|
920 self.StartTick = 0 |
|
921 for panel in self.GraphicPanels: |
|
922 panel.ResetItemsData() |
|
923 self.ResetCursorTick() |
|
924 |
|
925 def RefreshGraphicsWindowScrollbars(self): |
|
926 xstart, ystart = self.GraphicsWindow.GetViewStart() |
|
927 window_size = self.GraphicsWindow.GetClientSize() |
|
928 vwidth, vheight = self.GraphicsSizer.GetMinSize() |
|
929 posx = max(0, min(xstart, (vwidth - window_size[0]) / SCROLLBAR_UNIT)) |
|
930 posy = max(0, min(ystart, (vheight - window_size[1]) / SCROLLBAR_UNIT)) |
|
931 self.GraphicsWindow.Scroll(posx, posy) |
|
932 self.GraphicsWindow.SetScrollbars(SCROLLBAR_UNIT, SCROLLBAR_UNIT, |
|
933 vwidth / SCROLLBAR_UNIT, vheight / SCROLLBAR_UNIT, posx, posy) |
|
934 |
|
935 def OnGraphicsWindowEraseBackground(self, event): |
|
936 pass |
|
937 |
|
938 def OnGraphicsWindowPaint(self, event): |
|
939 self.RefreshView() |
|
940 event.Skip() |
|
941 |
|
942 def OnGraphicsWindowResize(self, event): |
|
943 size = self.GetSize() |
|
944 for panel in self.GraphicPanels: |
|
945 panel_size = panel.GetSize() |
|
946 if (isinstance(panel, DebugVariableGraphicViewer) and |
|
947 panel.GraphType == GRAPH_ORTHOGONAL and |
|
948 panel_size.width == panel_size.height): |
|
949 panel.SetCanvasHeight(size.width) |
|
950 self.RefreshGraphicsWindowScrollbars() |
|
951 self.GraphicsSizer.Layout() |
|
952 event.Skip() |
|
953 |
|
954 def OnGraphicsWindowMouseWheel(self, event): |
|
955 if self.VetoScrollEvent: |
|
956 self.VetoScrollEvent = False |
|
957 else: |
|
958 event.Skip() |
|