|
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 from time import time as gettime |
|
27 import numpy |
|
28 |
|
29 import wx |
|
30 |
|
31 import matplotlib |
|
32 matplotlib.use('WX') |
|
33 import matplotlib.pyplot |
|
34 from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas |
|
35 from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap |
|
36 from matplotlib.backends.backend_agg import FigureCanvasAgg |
|
37 from mpl_toolkits.mplot3d import Axes3D |
|
38 |
|
39 from editors.DebugViewer import REFRESH_PERIOD |
|
40 |
|
41 from DebugVariableItem import DebugVariableItem |
|
42 from DebugVariableViewer import * |
|
43 from GraphButton import GraphButton |
|
44 |
|
45 GRAPH_PARALLEL, GRAPH_ORTHOGONAL = range(2) |
|
46 |
|
47 #CANVAS_SIZE_TYPES |
|
48 [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI] = [0, 100, 200] |
|
49 |
|
50 DEFAULT_CANVAS_HEIGHT = 200. |
|
51 CANVAS_BORDER = (20., 10.) |
|
52 CANVAS_PADDING = 8.5 |
|
53 VALUE_LABEL_HEIGHT = 17. |
|
54 AXES_LABEL_HEIGHT = 12.75 |
|
55 |
|
56 COLOR_CYCLE = ['r', 'b', 'g', 'm', 'y', 'k'] |
|
57 CURSOR_COLOR = '#800080' |
|
58 |
|
59 def OrthogonalData(item, start_tick, end_tick): |
|
60 data = item.GetData(start_tick, end_tick) |
|
61 min_value, max_value = item.GetValueRange() |
|
62 if min_value is not None and max_value is not None: |
|
63 center = (min_value + max_value) / 2. |
|
64 range = max(1.0, max_value - min_value) |
|
65 else: |
|
66 center = 0.5 |
|
67 range = 1.0 |
|
68 return data, center - range * 0.55, center + range * 0.55 |
|
69 |
|
70 class DebugVariableDropTarget(wx.TextDropTarget): |
|
71 |
|
72 def __init__(self, parent, window): |
|
73 wx.TextDropTarget.__init__(self) |
|
74 self.ParentControl = parent |
|
75 self.ParentWindow = window |
|
76 |
|
77 def __del__(self): |
|
78 self.ParentControl = None |
|
79 self.ParentWindow = None |
|
80 |
|
81 def OnDragOver(self, x, y, d): |
|
82 self.ParentControl.OnMouseDragging(x, y) |
|
83 return wx.TextDropTarget.OnDragOver(self, x, y, d) |
|
84 |
|
85 def OnDropText(self, x, y, data): |
|
86 message = None |
|
87 try: |
|
88 values = eval(data) |
|
89 if not isinstance(values, TupleType): |
|
90 raise |
|
91 except: |
|
92 message = _("Invalid value \"%s\" for debug variable")%data |
|
93 values = None |
|
94 |
|
95 if message is not None: |
|
96 wx.CallAfter(self.ShowMessage, message) |
|
97 |
|
98 elif values[1] == "debug": |
|
99 width, height = self.ParentControl.GetSize() |
|
100 target_idx = self.ParentControl.GetIndex() |
|
101 merge_type = GRAPH_PARALLEL |
|
102 if self.ParentControl.Is3DCanvas(): |
|
103 if y > height / 2: |
|
104 target_idx += 1 |
|
105 if len(values) > 1 and values[2] == "move": |
|
106 self.ParentWindow.MoveValue(values[0], target_idx) |
|
107 else: |
|
108 self.ParentWindow.InsertValue(values[0], target_idx, force=True) |
|
109 |
|
110 else: |
|
111 rect = self.ParentControl.GetAxesBoundingBox() |
|
112 if rect.InsideXY(x, y): |
|
113 merge_rect = wx.Rect(rect.x, rect.y, rect.width / 2., rect.height) |
|
114 if merge_rect.InsideXY(x, y): |
|
115 merge_type = GRAPH_ORTHOGONAL |
|
116 wx.CallAfter(self.ParentWindow.MergeGraphs, values[0], target_idx, merge_type, force=True) |
|
117 else: |
|
118 if y > height / 2: |
|
119 target_idx += 1 |
|
120 if len(values) > 2 and values[2] == "move": |
|
121 self.ParentWindow.MoveValue(values[0], target_idx) |
|
122 else: |
|
123 self.ParentWindow.InsertValue(values[0], target_idx, force=True) |
|
124 |
|
125 def OnLeave(self): |
|
126 self.ParentWindow.ResetHighlight() |
|
127 return wx.TextDropTarget.OnLeave(self) |
|
128 |
|
129 def ShowMessage(self, message): |
|
130 dialog = wx.MessageDialog(self.ParentWindow, message, _("Error"), wx.OK|wx.ICON_ERROR) |
|
131 dialog.ShowModal() |
|
132 dialog.Destroy() |
|
133 |
|
134 |
|
135 class DebugVariableGraphicViewer(DebugVariableViewer, FigureCanvas): |
|
136 |
|
137 def __init__(self, parent, window, items, graph_type): |
|
138 DebugVariableViewer.__init__(self, window, items) |
|
139 |
|
140 self.CanvasSize = SIZE_MINI |
|
141 self.GraphType = graph_type |
|
142 self.CursorTick = None |
|
143 self.MouseStartPos = None |
|
144 self.StartCursorTick = None |
|
145 self.CanvasStartSize = None |
|
146 self.ContextualButtons = [] |
|
147 self.ContextualButtonsItem = None |
|
148 |
|
149 self.Figure = matplotlib.figure.Figure(facecolor='w') |
|
150 self.Figure.subplotpars.update(top=0.95, left=0.1, bottom=0.1, right=0.95) |
|
151 |
|
152 FigureCanvas.__init__(self, parent, -1, self.Figure) |
|
153 self.SetWindowStyle(wx.WANTS_CHARS) |
|
154 self.SetBackgroundColour(wx.WHITE) |
|
155 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) |
|
156 self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter) |
|
157 self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) |
|
158 self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) |
|
159 self.Bind(wx.EVT_SIZE, self.OnResize) |
|
160 |
|
161 canvas_size = self.GetCanvasMinSize() |
|
162 self.SetMinSize(canvas_size) |
|
163 self.SetDropTarget(DebugVariableDropTarget(self, window)) |
|
164 self.mpl_connect('button_press_event', self.OnCanvasButtonPressed) |
|
165 self.mpl_connect('motion_notify_event', self.OnCanvasMotion) |
|
166 self.mpl_connect('button_release_event', self.OnCanvasButtonReleased) |
|
167 self.mpl_connect('scroll_event', self.OnCanvasScroll) |
|
168 |
|
169 for size, bitmap in zip([SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI], |
|
170 ["minimize_graph", "middle_graph", "maximize_graph"]): |
|
171 self.Buttons.append(GraphButton(0, 0, bitmap, self.GetOnChangeSizeButton(size))) |
|
172 for bitmap, callback in [("export_graph_mini", self.OnExportGraphButton), |
|
173 ("delete_graph", self.OnCloseButton)]: |
|
174 self.Buttons.append(GraphButton(0, 0, bitmap, callback)) |
|
175 |
|
176 self.ResetGraphics() |
|
177 self.RefreshLabelsPosition(canvas_size.height) |
|
178 self.ShowButtons(False) |
|
179 |
|
180 def draw(self, drawDC=None): |
|
181 FigureCanvasAgg.draw(self) |
|
182 |
|
183 self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None) |
|
184 self.bitmap.UseAlpha() |
|
185 width, height = self.GetSize() |
|
186 bbox = self.GetAxesBoundingBox() |
|
187 |
|
188 destDC = wx.MemoryDC() |
|
189 destDC.SelectObject(self.bitmap) |
|
190 |
|
191 destGC = wx.GCDC(destDC) |
|
192 |
|
193 destGC.BeginDrawing() |
|
194 if self.Highlight == HIGHLIGHT_RESIZE: |
|
195 destGC.SetPen(HIGHLIGHT_RESIZE_PEN) |
|
196 destGC.SetBrush(HIGHLIGHT_RESIZE_BRUSH) |
|
197 destGC.DrawRectangle(0, height - 5, width, 5) |
|
198 else: |
|
199 destGC.SetPen(HIGHLIGHT_DROP_PEN) |
|
200 destGC.SetBrush(HIGHLIGHT_DROP_BRUSH) |
|
201 if self.Highlight == HIGHLIGHT_LEFT: |
|
202 destGC.DrawRectangle(bbox.x, bbox.y, |
|
203 bbox.width / 2, bbox.height) |
|
204 elif self.Highlight == HIGHLIGHT_RIGHT: |
|
205 destGC.DrawRectangle(bbox.x + bbox.width / 2, bbox.y, |
|
206 bbox.width / 2, bbox.height) |
|
207 |
|
208 self.DrawCommonElements(destGC, self.GetButtons()) |
|
209 |
|
210 destGC.EndDrawing() |
|
211 |
|
212 self._isDrawn = True |
|
213 self.gui_repaint(drawDC=drawDC) |
|
214 |
|
215 def GetButtons(self): |
|
216 return self.Buttons + self.ContextualButtons |
|
217 |
|
218 def PopupContextualButtons(self, item, rect, style=wx.RIGHT): |
|
219 if self.ContextualButtonsItem is not None and item != self.ContextualButtonsItem: |
|
220 self.DismissContextualButtons() |
|
221 |
|
222 if self.ContextualButtonsItem is None: |
|
223 self.ContextualButtonsItem = item |
|
224 |
|
225 if self.ContextualButtonsItem.IsForced(): |
|
226 self.ContextualButtons.append( |
|
227 GraphButton(0, 0, "release", self.OnReleaseButton)) |
|
228 for bitmap, callback in [("force", self.OnForceButton), |
|
229 ("export_graph_mini", self.OnExportItemGraphButton), |
|
230 ("delete_graph", self.OnRemoveItemButton)]: |
|
231 self.ContextualButtons.append(GraphButton(0, 0, bitmap, callback)) |
|
232 |
|
233 offset = 0 |
|
234 buttons = self.ContextualButtons[:] |
|
235 if style in [wx.TOP, wx.LEFT]: |
|
236 buttons.reverse() |
|
237 for button in buttons: |
|
238 w, h = button.GetSize() |
|
239 if style in [wx.LEFT, wx.RIGHT]: |
|
240 x = rect.x + (- w - offset |
|
241 if style == wx.LEFT |
|
242 else rect.width + offset) |
|
243 y = rect.y + (rect.height - h) / 2 |
|
244 offset += w |
|
245 else: |
|
246 x = rect.x + (rect.width - w ) / 2 |
|
247 y = rect.y + (- h - offset |
|
248 if style == wx.TOP |
|
249 else rect.height + offset) |
|
250 offset += h |
|
251 button.SetPosition(x, y) |
|
252 self.ParentWindow.ForceRefresh() |
|
253 |
|
254 def DismissContextualButtons(self): |
|
255 if self.ContextualButtonsItem is not None: |
|
256 self.ContextualButtonsItem = None |
|
257 self.ContextualButtons = [] |
|
258 self.ParentWindow.ForceRefresh() |
|
259 |
|
260 def IsOverContextualButton(self, x, y): |
|
261 for button in self.ContextualButtons: |
|
262 if button.HitTest(x, y): |
|
263 return True |
|
264 return False |
|
265 |
|
266 def SetMinSize(self, size): |
|
267 wx.Window.SetMinSize(self, size) |
|
268 wx.CallAfter(self.RefreshButtonsPosition) |
|
269 |
|
270 def GetOnChangeSizeButton(self, size): |
|
271 def OnChangeSizeButton(): |
|
272 self.CanvasSize = size |
|
273 self.SetCanvasSize(200, self.CanvasSize) |
|
274 return OnChangeSizeButton |
|
275 |
|
276 def OnExportGraphButton(self): |
|
277 self.ExportGraph() |
|
278 |
|
279 def OnForceButton(self): |
|
280 self.ForceValue(self.ContextualButtonsItem) |
|
281 self.DismissContextualButtons() |
|
282 |
|
283 def OnReleaseButton(self): |
|
284 self.ReleaseValue(self.ContextualButtonsItem) |
|
285 self.DismissContextualButtons() |
|
286 |
|
287 def OnExportItemGraphButton(self): |
|
288 self.ExportGraph(self.ContextualButtonsItem) |
|
289 self.DismissContextualButtons() |
|
290 |
|
291 def OnRemoveItemButton(self): |
|
292 wx.CallAfter(self.ParentWindow.DeleteValue, self, |
|
293 self.ContextualButtonsItem) |
|
294 self.DismissContextualButtons() |
|
295 |
|
296 def OnLeave(self, event): |
|
297 if self.Highlight != HIGHLIGHT_RESIZE or self.CanvasStartSize is None: |
|
298 DebugVariableViewer.OnLeave(self, event) |
|
299 else: |
|
300 event.Skip() |
|
301 |
|
302 def RefreshLabelsPosition(self, height): |
|
303 canvas_ratio = 1. / height |
|
304 graph_ratio = 1. / ((1.0 - (CANVAS_BORDER[0] + CANVAS_BORDER[1]) * canvas_ratio) * height) |
|
305 |
|
306 self.Figure.subplotpars.update( |
|
307 top= 1.0 - CANVAS_BORDER[1] * canvas_ratio, |
|
308 bottom= CANVAS_BORDER[0] * canvas_ratio) |
|
309 |
|
310 if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): |
|
311 num_item = len(self.Items) |
|
312 for idx in xrange(num_item): |
|
313 if not self.Is3DCanvas(): |
|
314 self.AxesLabels[idx].set_position( |
|
315 (0.05, |
|
316 1.0 - (CANVAS_PADDING + AXES_LABEL_HEIGHT * idx) * graph_ratio)) |
|
317 self.Labels[idx].set_position( |
|
318 (0.95, |
|
319 CANVAS_PADDING * graph_ratio + |
|
320 (num_item - idx - 1) * VALUE_LABEL_HEIGHT * graph_ratio)) |
|
321 else: |
|
322 self.AxesLabels[0].set_position((0.1, CANVAS_PADDING * graph_ratio)) |
|
323 self.Labels[0].set_position((0.95, CANVAS_PADDING * graph_ratio)) |
|
324 self.AxesLabels[1].set_position((0.05, 2 * CANVAS_PADDING * graph_ratio)) |
|
325 self.Labels[1].set_position((0.05, 1.0 - CANVAS_PADDING * graph_ratio)) |
|
326 |
|
327 self.Figure.subplots_adjust() |
|
328 |
|
329 def GetCanvasMinSize(self): |
|
330 return wx.Size(200, |
|
331 CANVAS_BORDER[0] + CANVAS_BORDER[1] + |
|
332 2 * CANVAS_PADDING + VALUE_LABEL_HEIGHT * len(self.Items)) |
|
333 |
|
334 def SetCanvasSize(self, width, height): |
|
335 height = max(height, self.GetCanvasMinSize()[1]) |
|
336 self.SetMinSize(wx.Size(width, height)) |
|
337 self.RefreshLabelsPosition(height) |
|
338 self.RefreshButtonsPosition() |
|
339 self.ParentWindow.RefreshGraphicsSizer() |
|
340 |
|
341 def GetAxesBoundingBox(self, absolute=False): |
|
342 width, height = self.GetSize() |
|
343 ax, ay, aw, ah = self.figure.gca().get_position().bounds |
|
344 bbox = wx.Rect(ax * width, height - (ay + ah) * height - 1, |
|
345 aw * width + 2, ah * height + 1) |
|
346 if absolute: |
|
347 xw, yw = self.GetPosition() |
|
348 bbox.x += xw |
|
349 bbox.y += yw |
|
350 return bbox |
|
351 |
|
352 def OnCanvasButtonPressed(self, event): |
|
353 width, height = self.GetSize() |
|
354 x, y = event.x, height - event.y |
|
355 if not self.IsOverButton(x, y): |
|
356 if event.inaxes == self.Axes: |
|
357 item_idx = None |
|
358 for i, t in ([pair for pair in enumerate(self.AxesLabels)] + |
|
359 [pair for pair in enumerate(self.Labels)]): |
|
360 (x0, y0), (x1, y1) = t.get_window_extent().get_points() |
|
361 rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) |
|
362 if rect.InsideXY(x, y): |
|
363 item_idx = i |
|
364 break |
|
365 if item_idx is not None: |
|
366 self.ShowButtons(False) |
|
367 self.DismissContextualButtons() |
|
368 xw, yw = self.GetPosition() |
|
369 self.ParentWindow.StartDragNDrop(self, |
|
370 self.ItemsDict.values()[item_idx], x + xw, y + yw, x + xw, y + yw) |
|
371 elif not self.Is3DCanvas(): |
|
372 self.MouseStartPos = wx.Point(x, y) |
|
373 if event.button == 1 and event.inaxes == self.Axes: |
|
374 self.StartCursorTick = self.CursorTick |
|
375 self.HandleCursorMove(event) |
|
376 elif event.button == 2 and self.GraphType == GRAPH_PARALLEL: |
|
377 width, height = self.GetSize() |
|
378 start_tick, end_tick = self.ParentWindow.GetRange() |
|
379 self.StartCursorTick = start_tick |
|
380 |
|
381 elif event.button == 1 and event.y <= 5: |
|
382 self.MouseStartPos = wx.Point(x, y) |
|
383 self.CanvasStartSize = self.GetSize() |
|
384 |
|
385 def OnCanvasButtonReleased(self, event): |
|
386 if self.ParentWindow.IsDragging(): |
|
387 width, height = self.GetSize() |
|
388 xw, yw = self.GetPosition() |
|
389 self.ParentWindow.StopDragNDrop( |
|
390 self.ParentWindow.DraggingAxesPanel.ItemsDict.values()[0].GetVariable(), |
|
391 xw + event.x, |
|
392 yw + height - event.y) |
|
393 else: |
|
394 self.MouseStartPos = None |
|
395 self.StartCursorTick = None |
|
396 self.CanvasStartSize = None |
|
397 width, height = self.GetSize() |
|
398 self.HandleButton(event.x, height - event.y) |
|
399 if event.y > 5 and self.SetHighlight(HIGHLIGHT_NONE): |
|
400 self.SetCursor(wx.NullCursor) |
|
401 self.ParentWindow.ForceRefresh() |
|
402 |
|
403 def OnCanvasMotion(self, event): |
|
404 width, height = self.GetSize() |
|
405 if self.ParentWindow.IsDragging(): |
|
406 xw, yw = self.GetPosition() |
|
407 self.ParentWindow.MoveDragNDrop( |
|
408 xw + event.x, |
|
409 yw + height - event.y) |
|
410 else: |
|
411 if not self.Is3DCanvas(): |
|
412 if event.button == 1 and self.CanvasStartSize is None: |
|
413 if event.inaxes == self.Axes: |
|
414 if self.MouseStartPos is not None: |
|
415 self.HandleCursorMove(event) |
|
416 elif self.MouseStartPos is not None and len(self.Items) == 1: |
|
417 xw, yw = self.GetPosition() |
|
418 self.ParentWindow.SetCursorTick(self.StartCursorTick) |
|
419 self.ParentWindow.StartDragNDrop(self, |
|
420 self.ItemsDict.values()[0], |
|
421 event.x + xw, height - event.y + yw, |
|
422 self.MouseStartPos.x + xw, self.MouseStartPos.y + yw) |
|
423 elif event.button == 2 and self.GraphType == GRAPH_PARALLEL and self.MouseStartPos is not None: |
|
424 start_tick, end_tick = self.ParentWindow.GetRange() |
|
425 rect = self.GetAxesBoundingBox() |
|
426 self.ParentWindow.SetCanvasPosition( |
|
427 self.StartCursorTick + (self.MouseStartPos.x - event.x) * |
|
428 (end_tick - start_tick) / rect.width) |
|
429 |
|
430 if event.button == 1 and self.CanvasStartSize is not None: |
|
431 width, height = self.GetSize() |
|
432 self.SetCanvasSize(width, |
|
433 self.CanvasStartSize.height + height - event.y - self.MouseStartPos.y) |
|
434 |
|
435 elif event.button in [None, "up", "down"]: |
|
436 if self.GraphType == GRAPH_PARALLEL: |
|
437 orientation = [wx.RIGHT] * len(self.AxesLabels) + [wx.LEFT] * len(self.Labels) |
|
438 elif len(self.AxesLabels) > 0: |
|
439 orientation = [wx.RIGHT, wx.TOP, wx.LEFT, wx.BOTTOM] |
|
440 else: |
|
441 orientation = [wx.LEFT] * len(self.Labels) |
|
442 item_idx = None |
|
443 item_style = None |
|
444 for (i, t), style in zip([pair for pair in enumerate(self.AxesLabels)] + |
|
445 [pair for pair in enumerate(self.Labels)], |
|
446 orientation): |
|
447 (x0, y0), (x1, y1) = t.get_window_extent().get_points() |
|
448 rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) |
|
449 if rect.InsideXY(event.x, height - event.y): |
|
450 item_idx = i |
|
451 item_style = style |
|
452 break |
|
453 if item_idx is not None: |
|
454 self.PopupContextualButtons(self.ItemsDict.values()[item_idx], rect, item_style) |
|
455 return |
|
456 if not self.IsOverContextualButton(event.x, height - event.y): |
|
457 self.DismissContextualButtons() |
|
458 |
|
459 if event.y <= 5: |
|
460 if self.SetHighlight(HIGHLIGHT_RESIZE): |
|
461 self.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS)) |
|
462 self.ParentWindow.ForceRefresh() |
|
463 else: |
|
464 if self.SetHighlight(HIGHLIGHT_NONE): |
|
465 self.SetCursor(wx.NullCursor) |
|
466 self.ParentWindow.ForceRefresh() |
|
467 |
|
468 def OnCanvasScroll(self, event): |
|
469 if event.inaxes is not None and event.guiEvent.ControlDown(): |
|
470 if self.GraphType == GRAPH_ORTHOGONAL: |
|
471 start_tick, end_tick = self.ParentWindow.GetRange() |
|
472 tick = (start_tick + end_tick) / 2. |
|
473 else: |
|
474 tick = event.xdata |
|
475 self.ParentWindow.ChangeRange(int(-event.step) / 3, tick) |
|
476 self.ParentWindow.VetoScrollEvent = True |
|
477 |
|
478 def RefreshHighlight(self, x, y): |
|
479 width, height = self.GetSize() |
|
480 bbox = self.GetAxesBoundingBox() |
|
481 if bbox.InsideXY(x, y) and not self.Is3DCanvas(): |
|
482 rect = wx.Rect(bbox.x, bbox.y, bbox.width / 2, bbox.height) |
|
483 if rect.InsideXY(x, y): |
|
484 self.SetHighlight(HIGHLIGHT_LEFT) |
|
485 else: |
|
486 self.SetHighlight(HIGHLIGHT_RIGHT) |
|
487 elif y < height / 2: |
|
488 if self.ParentWindow.IsViewerFirst(self): |
|
489 self.SetHighlight(HIGHLIGHT_BEFORE) |
|
490 else: |
|
491 self.SetHighlight(HIGHLIGHT_NONE) |
|
492 self.ParentWindow.HighlightPreviousViewer(self) |
|
493 else: |
|
494 self.SetHighlight(HIGHLIGHT_AFTER) |
|
495 |
|
496 def OnLeave(self, event): |
|
497 if self.CanvasStartSize is None and self.SetHighlight(HIGHLIGHT_NONE): |
|
498 self.SetCursor(wx.NullCursor) |
|
499 self.ParentWindow.ForceRefresh() |
|
500 DebugVariableViewer.OnLeave(self, event) |
|
501 |
|
502 KEY_CURSOR_INCREMENT = { |
|
503 wx.WXK_LEFT: -1, |
|
504 wx.WXK_RIGHT: 1, |
|
505 wx.WXK_UP: -10, |
|
506 wx.WXK_DOWN: 10} |
|
507 def OnKeyDown(self, event): |
|
508 if self.CursorTick is not None: |
|
509 move = self.KEY_CURSOR_INCREMENT.get(event.GetKeyCode(), None) |
|
510 if move is not None: |
|
511 self.ParentWindow.MoveCursorTick(move) |
|
512 event.Skip() |
|
513 |
|
514 def HandleCursorMove(self, event): |
|
515 start_tick, end_tick = self.ParentWindow.GetRange() |
|
516 cursor_tick = None |
|
517 items = self.ItemsDict.values() |
|
518 if self.GraphType == GRAPH_ORTHOGONAL: |
|
519 x_data = items[0].GetData(start_tick, end_tick) |
|
520 y_data = items[1].GetData(start_tick, end_tick) |
|
521 if len(x_data) > 0 and len(y_data) > 0: |
|
522 length = min(len(x_data), len(y_data)) |
|
523 d = numpy.sqrt((x_data[:length,1]-event.xdata) ** 2 + (y_data[:length,1]-event.ydata) ** 2) |
|
524 cursor_tick = x_data[numpy.argmin(d), 0] |
|
525 else: |
|
526 data = items[0].GetData(start_tick, end_tick) |
|
527 if len(data) > 0: |
|
528 cursor_tick = data[numpy.argmin(numpy.abs(data[:,0] - event.xdata)), 0] |
|
529 if cursor_tick is not None: |
|
530 self.ParentWindow.SetCursorTick(cursor_tick) |
|
531 |
|
532 def OnAxesMotion(self, event): |
|
533 if self.Is3DCanvas(): |
|
534 current_time = gettime() |
|
535 if current_time - self.LastMotionTime > REFRESH_PERIOD: |
|
536 self.LastMotionTime = current_time |
|
537 Axes3D._on_move(self.Axes, event) |
|
538 |
|
539 def ResetGraphics(self): |
|
540 self.Figure.clear() |
|
541 if self.Is3DCanvas(): |
|
542 self.Axes = self.Figure.gca(projection='3d') |
|
543 self.Axes.set_color_cycle(['b']) |
|
544 self.LastMotionTime = gettime() |
|
545 setattr(self.Axes, "_on_move", self.OnAxesMotion) |
|
546 self.Axes.mouse_init() |
|
547 self.Axes.tick_params(axis='z', labelsize='small') |
|
548 else: |
|
549 self.Axes = self.Figure.gca() |
|
550 self.Axes.set_color_cycle(COLOR_CYCLE) |
|
551 self.Axes.tick_params(axis='x', labelsize='small') |
|
552 self.Axes.tick_params(axis='y', labelsize='small') |
|
553 self.Plots = [] |
|
554 self.VLine = None |
|
555 self.HLine = None |
|
556 self.Labels = [] |
|
557 self.AxesLabels = [] |
|
558 if not self.Is3DCanvas(): |
|
559 text_func = self.Axes.text |
|
560 else: |
|
561 text_func = self.Axes.text2D |
|
562 if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): |
|
563 num_item = len(self.Items) |
|
564 for idx in xrange(num_item): |
|
565 if num_item == 1: |
|
566 color = 'k' |
|
567 else: |
|
568 color = COLOR_CYCLE[idx % len(COLOR_CYCLE)] |
|
569 if not self.Is3DCanvas(): |
|
570 self.AxesLabels.append( |
|
571 text_func(0, 0, "", size='small', |
|
572 verticalalignment='top', |
|
573 color=color, |
|
574 transform=self.Axes.transAxes)) |
|
575 self.Labels.append( |
|
576 text_func(0, 0, "", size='large', |
|
577 horizontalalignment='right', |
|
578 color=color, |
|
579 transform=self.Axes.transAxes)) |
|
580 else: |
|
581 self.AxesLabels.append( |
|
582 self.Axes.text(0, 0, "", size='small', |
|
583 transform=self.Axes.transAxes)) |
|
584 self.Labels.append( |
|
585 self.Axes.text(0, 0, "", size='large', |
|
586 horizontalalignment='right', |
|
587 transform=self.Axes.transAxes)) |
|
588 self.AxesLabels.append( |
|
589 self.Axes.text(0, 0, "", size='small', |
|
590 rotation='vertical', |
|
591 verticalalignment='bottom', |
|
592 transform=self.Axes.transAxes)) |
|
593 self.Labels.append( |
|
594 self.Axes.text(0, 0, "", size='large', |
|
595 rotation='vertical', |
|
596 verticalalignment='top', |
|
597 transform=self.Axes.transAxes)) |
|
598 width, height = self.GetSize() |
|
599 self.RefreshLabelsPosition(height) |
|
600 |
|
601 def AddItem(self, item): |
|
602 DebugVariableViewer.AddItem(self, item) |
|
603 self.ResetGraphics() |
|
604 |
|
605 def RemoveItem(self, item): |
|
606 DebugVariableViewer.RemoveItem(self, item) |
|
607 if not self.ItemsIsEmpty(): |
|
608 if len(self.Items) == 1: |
|
609 self.GraphType = GRAPH_PARALLEL |
|
610 self.ResetGraphics() |
|
611 |
|
612 def UnsubscribeObsoleteData(self): |
|
613 DebugVariableViewer.UnsubscribeObsoleteData(self) |
|
614 if not self.ItemsIsEmpty(): |
|
615 self.ResetGraphics() |
|
616 |
|
617 def Is3DCanvas(self): |
|
618 return self.GraphType == GRAPH_ORTHOGONAL and len(self.Items) == 3 |
|
619 |
|
620 def SetCursorTick(self, cursor_tick): |
|
621 self.CursorTick = cursor_tick |
|
622 |
|
623 def RefreshViewer(self, refresh_graphics=True): |
|
624 |
|
625 if refresh_graphics: |
|
626 start_tick, end_tick = self.ParentWindow.GetRange() |
|
627 |
|
628 if self.GraphType == GRAPH_PARALLEL: |
|
629 min_value = max_value = None |
|
630 |
|
631 for idx, item in enumerate(self.Items): |
|
632 data = item.GetData(start_tick, end_tick) |
|
633 if data is not None: |
|
634 item_min_value, item_max_value = item.GetValueRange() |
|
635 if min_value is None: |
|
636 min_value = item_min_value |
|
637 elif item_min_value is not None: |
|
638 min_value = min(min_value, item_min_value) |
|
639 if max_value is None: |
|
640 max_value = item_max_value |
|
641 elif item_max_value is not None: |
|
642 max_value = max(max_value, item_max_value) |
|
643 |
|
644 if len(self.Plots) <= idx: |
|
645 self.Plots.append( |
|
646 self.Axes.plot(data[:, 0], data[:, 1])[0]) |
|
647 else: |
|
648 self.Plots[idx].set_data(data[:, 0], data[:, 1]) |
|
649 |
|
650 if min_value is not None and max_value is not None: |
|
651 y_center = (min_value + max_value) / 2. |
|
652 y_range = max(1.0, max_value - min_value) |
|
653 else: |
|
654 y_center = 0.5 |
|
655 y_range = 1.0 |
|
656 x_min, x_max = start_tick, end_tick |
|
657 y_min, y_max = y_center - y_range * 0.55, y_center + y_range * 0.55 |
|
658 |
|
659 if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick: |
|
660 if self.VLine is None: |
|
661 self.VLine = self.Axes.axvline(self.CursorTick, color=CURSOR_COLOR) |
|
662 else: |
|
663 self.VLine.set_xdata((self.CursorTick, self.CursorTick)) |
|
664 self.VLine.set_visible(True) |
|
665 else: |
|
666 if self.VLine is not None: |
|
667 self.VLine.set_visible(False) |
|
668 else: |
|
669 min_start_tick = reduce(max, [item.GetData()[0, 0] |
|
670 for item in self.Items |
|
671 if len(item.GetData()) > 0], 0) |
|
672 start_tick = max(start_tick, min_start_tick) |
|
673 end_tick = max(end_tick, min_start_tick) |
|
674 items = self.ItemsDict.values() |
|
675 x_data, x_min, x_max = OrthogonalData(items[0], start_tick, end_tick) |
|
676 y_data, y_min, y_max = OrthogonalData(items[1], start_tick, end_tick) |
|
677 if self.CursorTick is not None: |
|
678 x_cursor, x_forced = items[0].GetValue(self.CursorTick, raw=True) |
|
679 y_cursor, y_forced = items[1].GetValue(self.CursorTick, raw=True) |
|
680 length = 0 |
|
681 if x_data is not None and y_data is not None: |
|
682 length = min(len(x_data), len(y_data)) |
|
683 if len(self.Items) < 3: |
|
684 if x_data is not None and y_data is not None: |
|
685 if len(self.Plots) == 0: |
|
686 self.Plots.append( |
|
687 self.Axes.plot(x_data[:, 1][:length], |
|
688 y_data[:, 1][:length])[0]) |
|
689 else: |
|
690 self.Plots[0].set_data( |
|
691 x_data[:, 1][:length], |
|
692 y_data[:, 1][:length]) |
|
693 |
|
694 if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick: |
|
695 if self.VLine is None: |
|
696 self.VLine = self.Axes.axvline(x_cursor, color=CURSOR_COLOR) |
|
697 else: |
|
698 self.VLine.set_xdata((x_cursor, x_cursor)) |
|
699 if self.HLine is None: |
|
700 self.HLine = self.Axes.axhline(y_cursor, color=CURSOR_COLOR) |
|
701 else: |
|
702 self.HLine.set_ydata((y_cursor, y_cursor)) |
|
703 self.VLine.set_visible(True) |
|
704 self.HLine.set_visible(True) |
|
705 else: |
|
706 if self.VLine is not None: |
|
707 self.VLine.set_visible(False) |
|
708 if self.HLine is not None: |
|
709 self.HLine.set_visible(False) |
|
710 else: |
|
711 while len(self.Axes.lines) > 0: |
|
712 self.Axes.lines.pop() |
|
713 z_data, z_min, z_max = OrthogonalData(items[2], start_tick, end_tick) |
|
714 if self.CursorTick is not None: |
|
715 z_cursor, z_forced = items[2].GetValue(self.CursorTick, raw=True) |
|
716 if x_data is not None and y_data is not None and z_data is not None: |
|
717 length = min(length, len(z_data)) |
|
718 self.Axes.plot(x_data[:, 1][:length], |
|
719 y_data[:, 1][:length], |
|
720 zs = z_data[:, 1][:length]) |
|
721 self.Axes.set_zlim(z_min, z_max) |
|
722 if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick: |
|
723 for kwargs in [{"xs": numpy.array([x_min, x_max])}, |
|
724 {"ys": numpy.array([y_min, y_max])}, |
|
725 {"zs": numpy.array([z_min, z_max])}]: |
|
726 for param, value in [("xs", numpy.array([x_cursor, x_cursor])), |
|
727 ("ys", numpy.array([y_cursor, y_cursor])), |
|
728 ("zs", numpy.array([z_cursor, z_cursor]))]: |
|
729 kwargs.setdefault(param, value) |
|
730 kwargs["color"] = CURSOR_COLOR |
|
731 self.Axes.plot(**kwargs) |
|
732 |
|
733 self.Axes.set_xlim(x_min, x_max) |
|
734 self.Axes.set_ylim(y_min, y_max) |
|
735 |
|
736 variable_name_mask = self.ParentWindow.GetVariableNameMask() |
|
737 if self.CursorTick is not None: |
|
738 values, forced = apply(zip, [item.GetValue(self.CursorTick) for item in self.Items]) |
|
739 else: |
|
740 values, forced = apply(zip, [(item.GetValue(), item.IsForced()) for item in self.Items]) |
|
741 labels = [item.GetVariable(variable_name_mask) for item in self.Items] |
|
742 styles = map(lambda x: {True: 'italic', False: 'normal'}[x], forced) |
|
743 if self.Is3DCanvas(): |
|
744 for idx, label_func in enumerate([self.Axes.set_xlabel, |
|
745 self.Axes.set_ylabel, |
|
746 self.Axes.set_zlabel]): |
|
747 label_func(labels[idx], fontdict={'size': 'small','color': COLOR_CYCLE[idx]}) |
|
748 else: |
|
749 for label, text in zip(self.AxesLabels, labels): |
|
750 label.set_text(text) |
|
751 for label, value, style in zip(self.Labels, values, styles): |
|
752 label.set_text(value) |
|
753 label.set_style(style) |
|
754 |
|
755 self.draw() |
|
756 |
|
757 def ExportGraph(self, item=None): |
|
758 if item is not None: |
|
759 variables = [(item, [entry for entry in item.GetData()])] |
|
760 else: |
|
761 variables = [(item, [entry for entry in item.GetData()]) |
|
762 for item in self.Items] |
|
763 self.ParentWindow.CopyDataToClipboard(variables) |