|
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 collections import OrderedDict |
|
26 |
|
27 import wx |
|
28 |
|
29 import matplotlib |
|
30 matplotlib.use('WX') |
|
31 import matplotlib.pyplot |
|
32 from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap |
|
33 |
|
34 from dialogs.ForceVariableDialog import ForceVariableDialog |
|
35 |
|
36 # Viewer highlight types |
|
37 [HIGHLIGHT_NONE, |
|
38 HIGHLIGHT_BEFORE, |
|
39 HIGHLIGHT_AFTER, |
|
40 HIGHLIGHT_LEFT, |
|
41 HIGHLIGHT_RIGHT, |
|
42 HIGHLIGHT_RESIZE] = range(6) |
|
43 |
|
44 # Viewer highlight styles |
|
45 HIGHLIGHT_DROP_PEN = wx.Pen(wx.Colour(0, 128, 255)) |
|
46 HIGHLIGHT_DROP_BRUSH = wx.Brush(wx.Colour(0, 128, 255, 128)) |
|
47 HIGHLIGHT_RESIZE_PEN = wx.Pen(wx.Colour(200, 200, 200)) |
|
48 HIGHLIGHT_RESIZE_BRUSH = wx.Brush(wx.Colour(200, 200, 200)) |
|
49 |
|
50 #------------------------------------------------------------------------------- |
|
51 # Base Debug Variable Viewer Class |
|
52 #------------------------------------------------------------------------------- |
|
53 |
|
54 """ |
|
55 Class that implements a generic viewer that display a list of variable values |
|
56 This class has to be inherited to effectively display variable values |
|
57 """ |
|
58 |
|
59 class DebugVariableViewer: |
|
60 |
|
61 def __init__(self, window, items=[]): |
|
62 """ |
|
63 Constructor |
|
64 @param window: Reference to the Debug Variable Panel |
|
65 @param items: List of DebugVariableItem displayed by Viewer |
|
66 """ |
|
67 self.ParentWindow = window |
|
68 self.ItemsDict = OrderedDict([(item.GetVariable(), item) |
|
69 for item in items]) |
|
70 self.Items = self.ItemsDict.viewvalues() |
|
71 |
|
72 # Variable storing current highlight displayed in Viewer |
|
73 self.Highlight = HIGHLIGHT_NONE |
|
74 # List of buttons |
|
75 self.Buttons = [] |
|
76 |
|
77 def __del__(self): |
|
78 """ |
|
79 Destructor |
|
80 """ |
|
81 # Remove reference to Debug Variable Panel |
|
82 self.ParentWindow = None |
|
83 |
|
84 def GetIndex(self): |
|
85 """ |
|
86 Return position of Viewer in Debug Variable Panel |
|
87 @return: Position of Viewer |
|
88 """ |
|
89 return self.ParentWindow.GetViewerIndex(self) |
|
90 |
|
91 def GetItem(self, variable): |
|
92 """ |
|
93 Return item storing values of a variable |
|
94 @param variable: Variable path |
|
95 @return: Item storing values of this variable |
|
96 """ |
|
97 return self.ItemsDict.get(variable, None) |
|
98 |
|
99 def GetItems(self): |
|
100 """ |
|
101 Return items displayed by Viewer |
|
102 @return: List of items displayed in Viewer |
|
103 """ |
|
104 return self.Items |
|
105 |
|
106 def AddItem(self, item): |
|
107 """ |
|
108 Add an item to the list of items displayed by Viewer |
|
109 @param item: Item to add to the list |
|
110 """ |
|
111 self.ItemsDict[item.GetVariable()] = item |
|
112 |
|
113 def RemoveItem(self, item): |
|
114 """ |
|
115 Remove an item from the list of items displayed by Viewer |
|
116 @param item: Item to remove from the list |
|
117 """ |
|
118 self.ItemsDict.pop(item.GetVariable(), None) |
|
119 |
|
120 def ClearItems(self): |
|
121 """ |
|
122 Clear list of items displayed by Viewer |
|
123 """ |
|
124 # Unsubscribe every items of the list |
|
125 for item in self.Items: |
|
126 self.ParentWindow.RemoveDataConsumer(item) |
|
127 |
|
128 # Clear list |
|
129 self.Items.clear() |
|
130 |
|
131 def ItemsIsEmpty(self): |
|
132 """ |
|
133 Return if list of items displayed by Viewer is empty |
|
134 @return: True if list is empty |
|
135 """ |
|
136 return len(self.Items) == 0 |
|
137 |
|
138 def UnsubscribeObsoleteData(self): |
|
139 """ |
|
140 Function that unsubscribe and remove every item that store values of |
|
141 a variable that doesn't exist in PLC anymore |
|
142 """ |
|
143 for item in self.ItemsDict.values()[:]: |
|
144 iec_path = item.GetVariable() |
|
145 |
|
146 # Check that variablepath exist in PLC |
|
147 if self.ParentWindow.GetDataType(iec_path) is None: |
|
148 # If not, unsubscribe and remove it |
|
149 self.ParentWindow.RemoveDataConsumer(item) |
|
150 self.RemoveItem(item) |
|
151 else: |
|
152 # If it exist, resubscribe and refresh data type |
|
153 self.ParentWindow.AddDataConsumer(iec_path.upper(), item) |
|
154 item.RefreshVariableType() |
|
155 |
|
156 def ResetItemsData(self): |
|
157 """ |
|
158 Reset data stored in every items displayed in Viewer |
|
159 """ |
|
160 for item in self.Items: |
|
161 item.ResetData() |
|
162 |
|
163 def RefreshViewer(self): |
|
164 """ |
|
165 Method that refresh the content displayed by Viewer |
|
166 Need to be overridden by inherited classes |
|
167 """ |
|
168 pass |
|
169 |
|
170 def SetHighlight(self, highlight): |
|
171 """ |
|
172 Set Highlight type displayed in Viewer |
|
173 @return: True if highlight has changed |
|
174 """ |
|
175 # Return immediately if highlight don't change |
|
176 if self.Highlight == highlight: |
|
177 return False |
|
178 |
|
179 self.Highlight = highlight |
|
180 return True |
|
181 |
|
182 def GetButtons(self): |
|
183 """ |
|
184 Return list of buttons defined in Viewer |
|
185 @return: List of buttons |
|
186 """ |
|
187 return self.Buttons |
|
188 |
|
189 def IsOverButton(self, x, y): |
|
190 """ |
|
191 Return if point is over one button of Viewer |
|
192 @param x: X coordinate of point |
|
193 @param y: Y coordinate of point |
|
194 @return: button where point is over |
|
195 """ |
|
196 for button in self.GetButtons(): |
|
197 if button.HitTest(x, y): |
|
198 return button |
|
199 return None |
|
200 |
|
201 def HandleButton(self, x, y): |
|
202 """ |
|
203 Search for the button under point and if found execute associated |
|
204 callback |
|
205 @param x: X coordinate of point |
|
206 @param y: Y coordinate of point |
|
207 @return: True if a button was found and callback executed |
|
208 """ |
|
209 button = self.IsOverButton(x, y) |
|
210 if button is None: |
|
211 return False |
|
212 |
|
213 button.ProcessCallback() |
|
214 return True |
|
215 |
|
216 def ShowButtons(self, show): |
|
217 """ |
|
218 Set display state of buttons in Viewer |
|
219 @param show: Display state (True if buttons must be displayed) |
|
220 """ |
|
221 # Change display of every buttons |
|
222 for button in self.Buttons: |
|
223 button.Show(show) |
|
224 |
|
225 # Refresh button positions |
|
226 self.RefreshButtonsPosition() |
|
227 self.RefreshViewer() |
|
228 |
|
229 def RefreshButtonsPosition(self): |
|
230 """ |
|
231 Function that refresh buttons position in Viewer |
|
232 """ |
|
233 # Get Viewer size |
|
234 width, height = self.GetSize() |
|
235 |
|
236 # Buttons are align right so we calculate buttons positions in |
|
237 # reverse order |
|
238 buttons = self.Buttons[:] |
|
239 buttons.reverse() |
|
240 |
|
241 # Position offset on x coordinate |
|
242 x_offset = 0 |
|
243 for button in buttons: |
|
244 # Buttons are stacked right, removing those that are not active |
|
245 if button.IsEnabled(): |
|
246 # Update button position according to button width and offset |
|
247 # on x coordinate |
|
248 w, h = button.GetSize() |
|
249 button.SetPosition(width - 5 - w - x_offset, 5) |
|
250 # Update offset on x coordinate |
|
251 x_offset += w + 2 |
|
252 |
|
253 def DrawCommonElements(self, dc, buttons=None): |
|
254 """ |
|
255 Function that draw common graphics for every Viewers |
|
256 @param dc: wx.DC object corresponding to Device context where drawing |
|
257 common graphics |
|
258 @param buttons: List of buttons to draw if different from default |
|
259 (default None) |
|
260 """ |
|
261 # Get Viewer size |
|
262 width, height = self.GetSize() |
|
263 |
|
264 # Set dc styling for drop before or drop after highlight |
|
265 dc.SetPen(HIGHLIGHT_DROP_PEN) |
|
266 dc.SetBrush(HIGHLIGHT_DROP_BRUSH) |
|
267 |
|
268 # Draw line at upper side of Viewer if highlight is drop before |
|
269 if self.Highlight == HIGHLIGHT_BEFORE: |
|
270 dc.DrawLine(0, 1, width - 1, 1) |
|
271 |
|
272 # Draw line at lower side of Viewer if highlight is drop before |
|
273 elif self.Highlight == HIGHLIGHT_AFTER: |
|
274 dc.DrawLine(0, height - 1, width - 1, height - 1) |
|
275 |
|
276 # If no specific buttons are defined, get default buttons |
|
277 if buttons is None: |
|
278 buttons = self.Buttons |
|
279 # Draw buttons |
|
280 for button in buttons: |
|
281 button.Draw(dc) |
|
282 |
|
283 # If graph dragging is processing |
|
284 if self.ParentWindow.IsDragging(): |
|
285 destBBox = self.ParentWindow.GetDraggingAxesClippingRegion(self) |
|
286 srcPos = self.ParentWindow.GetDraggingAxesPosition(self) |
|
287 if destBBox.width > 0 and destBBox.height > 0: |
|
288 srcPanel = self.ParentWindow.DraggingAxesPanel |
|
289 srcBBox = srcPanel.GetAxesBoundingBox() |
|
290 |
|
291 srcX = srcBBox.x - (srcPos.x if destBBox.x == 0 else 0) |
|
292 srcY = srcBBox.y - (srcPos.y if destBBox.y == 0 else 0) |
|
293 |
|
294 srcBmp = _convert_agg_to_wx_bitmap( |
|
295 srcPanel.get_renderer(), None) |
|
296 srcDC = wx.MemoryDC() |
|
297 srcDC.SelectObject(srcBmp) |
|
298 |
|
299 dc.Blit(destBBox.x, destBBox.y, |
|
300 int(destBBox.width), int(destBBox.height), |
|
301 srcDC, srcX, srcY) |
|
302 |
|
303 def OnEnter(self, event): |
|
304 """ |
|
305 Function called when entering Viewer |
|
306 @param event: wx.MouseEvent |
|
307 """ |
|
308 # Display buttons |
|
309 self.ShowButtons(True) |
|
310 event.Skip() |
|
311 |
|
312 def OnLeave(self, event): |
|
313 """ |
|
314 Function called when leaving Viewer |
|
315 @param event: wx.MouseEvent |
|
316 """ |
|
317 # Check that mouse position is inside Viewer and hide buttons |
|
318 x, y = event.GetPosition() |
|
319 width, height = self.GetSize() |
|
320 if not (0 < x < width - 1 and 0 < y < height - 1): |
|
321 self.ShowButtons(False) |
|
322 event.Skip() |
|
323 |
|
324 def OnCloseButton(self): |
|
325 """ |
|
326 Function called when Close button is pressed |
|
327 """ |
|
328 wx.CallAfter(self.ParentWindow.DeleteValue, self) |
|
329 |
|
330 def OnForceButton(self): |
|
331 """ |
|
332 Function called when Force button is pressed |
|
333 """ |
|
334 self.ForceValue(self.ItemsDict.values()[0]) |
|
335 |
|
336 def OnReleaseButton(self): |
|
337 """ |
|
338 Function called when Release button is pressed |
|
339 """ |
|
340 self.ReleaseValue(self.ItemsDict.values()[0]) |
|
341 |
|
342 def OnMouseDragging(self, x, y): |
|
343 """ |
|
344 Function called when mouse is dragged over Viewer |
|
345 @param x: X coordinate of mouse pointer |
|
346 @param y: Y coordinate of mouse pointer |
|
347 """ |
|
348 xw, yw = self.GetPosition() |
|
349 # Refresh highlight in Debug Variable Panel (highlight can be displayed |
|
350 # in another Viewer |
|
351 self.ParentWindow.RefreshHighlight(x + xw, y + yw) |
|
352 |
|
353 def RefreshHighlight(self, x, y): |
|
354 """ |
|
355 Function called by Debug Variable Panel asking Viewer to refresh |
|
356 highlight according to mouse position |
|
357 @param x: X coordinate of mouse pointer |
|
358 @param y: Y coordinate of mouse pointer |
|
359 """ |
|
360 # Get Viewer size |
|
361 width, height = self.GetSize() |
|
362 |
|
363 # Mouse is in the first half of Viewer |
|
364 if y < height / 2: |
|
365 # If Viewer is the upper one, draw drop before highlight |
|
366 if self.ParentWindow.IsViewerFirst(self): |
|
367 self.SetHighlight(HIGHLIGHT_BEFORE) |
|
368 |
|
369 # Else draw drop after highlight in previous Viewer |
|
370 else: |
|
371 self.SetHighlight(HIGHLIGHT_NONE) |
|
372 self.ParentWindow.HighlightPreviousViewer(self) |
|
373 |
|
374 # Mouse is in the second half of Viewer, draw drop after highlight |
|
375 else: |
|
376 self.SetHighlight(HIGHLIGHT_AFTER) |
|
377 |
|
378 def OnEraseBackground(self, event): |
|
379 """ |
|
380 Function called when Viewer background is going to be erase |
|
381 @param event: wx.EraseEvent |
|
382 """ |
|
383 # Prevent flicker on Windows |
|
384 pass |
|
385 |
|
386 def OnResize(self, event): |
|
387 """ |
|
388 Function called when Viewer size changed |
|
389 @param event: wx.ResizeEvent |
|
390 """ |
|
391 # Refresh button positions |
|
392 self.RefreshButtonsPosition() |
|
393 self.ParentWindow.ForceRefresh() |
|
394 event.Skip() |
|
395 |
|
396 def ForceValue(self, item): |
|
397 """ |
|
398 Force value of item given |
|
399 @param item: Item to force value |
|
400 """ |
|
401 # Check variable data type |
|
402 iec_path = item.GetVariable() |
|
403 iec_type = self.ParentWindow.GetDataType(iec_path) |
|
404 # Return immediately if not found |
|
405 if iec_type is None: |
|
406 return |
|
407 |
|
408 # Open a dialog to enter varaible forced value |
|
409 dialog = ForceVariableDialog(self, iec_type, str(item.GetValue())) |
|
410 if dialog.ShowModal() == wx.ID_OK: |
|
411 self.ParentWindow.ForceDataValue(iec_path.upper(), |
|
412 dialog.GetValue()) |
|
413 |
|
414 def ReleaseValue(self, item): |
|
415 """ |
|
416 Release value of item given |
|
417 @param item: Item to release value |
|
418 """ |
|
419 self.ParentWindow.ReleaseDataValue( |
|
420 item.GetVariable().upper()) |