Laurent@814: #!/usr/bin/env python
Laurent@814: # -*- coding: utf-8 -*-
Laurent@814: 
andrej@1571: # This file is part of Beremiz, a Integrated Development Environment for
andrej@1571: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
Laurent@814: #
andrej@1571: # Copyright (C) 2012: Edouard TISSERANT and Laurent BESSARD
Laurent@814: #
andrej@1571: # See COPYING file for copyrights details.
Laurent@814: #
andrej@1571: # This program is free software; you can redistribute it and/or
andrej@1571: # modify it under the terms of the GNU General Public License
andrej@1571: # as published by the Free Software Foundation; either version 2
andrej@1571: # of the License, or (at your option) any later version.
Laurent@814: #
andrej@1571: # This program is distributed in the hope that it will be useful,
andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1571: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@1571: # GNU General Public License for more details.
Laurent@814: #
andrej@1571: # You should have received a copy of the GNU General Public License
andrej@1571: # along with this program; if not, write to the Free Software
andrej@1571: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
Laurent@814: 
andrej@1881: 
kinsamanka@3750: 
kinsamanka@3750: 
Laurent@1200: from collections import OrderedDict
andrej@2456: from functools import reduce
Laurent@814: 
Laurent@814: import wx
Laurent@1200: 
Laurent@814: from dialogs.ForceVariableDialog import ForceVariableDialog
Laurent@1200: 
Laurent@1200: # Viewer highlight types
Laurent@1198: [HIGHLIGHT_NONE,
Laurent@1198:  HIGHLIGHT_BEFORE,
Laurent@1198:  HIGHLIGHT_AFTER,
Laurent@1198:  HIGHLIGHT_LEFT,
Laurent@1198:  HIGHLIGHT_RIGHT,
kinsamanka@3750:  HIGHLIGHT_RESIZE] = list(range(6))
Laurent@1198: 
Laurent@1200: # Viewer highlight styles
andrej@1823: HIGHLIGHT = {
andrej@1823: }
Laurent@1198: 
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1200: #                        Base Debug Variable Viewer Class
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1200: 
Laurent@1198: 
andrej@1831: class DebugVariableViewer(object):
andrej@1736:     """
andrej@1736:     Class that implements a generic viewer that display a list of variable values
andrej@1736:     This class has to be inherited to effectively display variable values
andrej@1736:     """
andrej@1730: 
andrej@1852:     def __init__(self, window, items=None):
Laurent@1200:         """
Laurent@1200:         Constructor
Laurent@1200:         @param window: Reference to the Debug Variable Panel
Laurent@1200:         @param items: List of DebugVariableItem displayed by Viewer
Laurent@1200:         """
Laurent@1198:         self.ParentWindow = window
andrej@1852:         items = [] if items is None else items
Laurent@1200:         self.ItemsDict = OrderedDict([(item.GetVariable(), item)
Laurent@1200:                                       for item in items])
kinsamanka@3750:         self.Items = self.ItemsDict.values()
andrej@1730: 
Laurent@1200:         # Variable storing current highlight displayed in Viewer
Laurent@1198:         self.Highlight = HIGHLIGHT_NONE
Laurent@1200:         # List of buttons
Laurent@1198:         self.Buttons = []
andrej@1807:         self.InitHighlightPensBrushes()
andrej@1730: 
andrej@1807:     def InitHighlightPensBrushes(self):
andrej@1807:         """
andrej@1807:         Init global pens and brushes
andrej@1807:         """
andrej@1823:         if not HIGHLIGHT:
andrej@1823:             HIGHLIGHT['DROP_PEN'] = wx.Pen(wx.Colour(0, 128, 255))
andrej@1823:             HIGHLIGHT['DROP_BRUSH'] = wx.Brush(wx.Colour(0, 128, 255, 128))
andrej@1823:             HIGHLIGHT['RESIZE_PEN'] = wx.Pen(wx.Colour(200, 200, 200))
andrej@1823:             HIGHLIGHT['RESIZE_BRUSH'] = wx.Brush(wx.Colour(200, 200, 200))
andrej@1807: 
Laurent@1198:     def GetIndex(self):
Laurent@1200:         """
Laurent@1200:         Return position of Viewer in Debug Variable Panel
Laurent@1200:         @return: Position of Viewer
Laurent@1200:         """
Laurent@1198:         return self.ParentWindow.GetViewerIndex(self)
andrej@1730: 
Laurent@1198:     def GetItem(self, variable):
Laurent@1200:         """
Laurent@1200:         Return item storing values of a variable
Laurent@1200:         @param variable: Variable path
Laurent@1200:         @return: Item storing values of this variable
Laurent@1200:         """
Laurent@1200:         return self.ItemsDict.get(variable, None)
andrej@1730: 
Laurent@1198:     def GetItems(self):
Laurent@1200:         """
Laurent@1200:         Return items displayed by Viewer
Laurent@1200:         @return: List of items displayed in Viewer
Laurent@1200:         """
kinsamanka@3750:         return list(self.ItemsDict.values())
andrej@1730: 
Laurent@1198:     def AddItem(self, item):
Laurent@1200:         """
Laurent@1200:         Add an item to the list of items displayed by Viewer
Laurent@1200:         @param item: Item to add to the list
Laurent@1200:         """
Laurent@1200:         self.ItemsDict[item.GetVariable()] = item
andrej@1730: 
Laurent@1198:     def RemoveItem(self, item):
Laurent@1200:         """
Laurent@1200:         Remove an item from the list of items displayed by Viewer
Laurent@1200:         @param item: Item to remove from the list
Laurent@1200:         """
Laurent@1200:         self.ItemsDict.pop(item.GetVariable(), None)
andrej@1730: 
Laurent@1200:     def ClearItems(self):
Laurent@1200:         """
Laurent@1200:         Clear list of items displayed by Viewer
Laurent@1200:         """
Laurent@1200:         # Unsubscribe every items of the list
Laurent@1198:         for item in self.Items:
Laurent@1198:             self.ParentWindow.RemoveDataConsumer(item)
andrej@1730: 
Laurent@1200:         # Clear list
Laurent@1206:         self.ItemsDict.clear()
andrej@1730: 
Laurent@1200:     def ItemsIsEmpty(self):
Laurent@1200:         """
Laurent@1200:         Return if list of items displayed by Viewer is empty
Laurent@1200:         @return: True if list is empty
Laurent@1200:         """
Laurent@1198:         return len(self.Items) == 0
andrej@1730: 
Laurent@1207:     def SubscribeAllDataConsumers(self):
Laurent@1200:         """
Laurent@1200:         Function that unsubscribe and remove every item that store values of
Laurent@1200:         a variable that doesn't exist in PLC anymore
Laurent@1200:         """
kinsamanka@3764:         for item in self.ItemsDict.values():
Laurent@1198:             iec_path = item.GetVariable()
andrej@1730: 
Laurent@1200:             # Check that variablepath exist in PLC
Laurent@1198:             if self.ParentWindow.GetDataType(iec_path) is None:
Laurent@1200:                 # If not, unsubscribe and remove it
Laurent@1198:                 self.ParentWindow.RemoveDataConsumer(item)
Laurent@1198:                 self.RemoveItem(item)
Laurent@1198:             else:
Laurent@1200:                 # If it exist, resubscribe and refresh data type
Laurent@1365:                 self.ParentWindow.AddDataConsumer(iec_path.upper(), item, True)
Laurent@1198:                 item.RefreshVariableType()
andrej@1730: 
Laurent@1200:     def ResetItemsData(self):
Laurent@1200:         """
Laurent@1200:         Reset data stored in every items displayed in Viewer
Laurent@1200:         """
Laurent@1198:         for item in self.Items:
Laurent@1198:             item.ResetData()
andrej@1730: 
Laurent@1212:     def GetItemsMinCommonTick(self):
Laurent@1212:         """
Laurent@1212:         Return the minimum tick common to all iems displayed in Viewer
Laurent@1212:         @return: Minimum common tick between items
Laurent@1212:         """
andrej@1730:         return reduce(max, [item.GetData()[0, 0]
Laurent@1212:                             for item in self.Items
Laurent@1212:                             if len(item.GetData()) > 0], 0)
andrej@1730: 
Laurent@1198:     def RefreshViewer(self):
Laurent@1200:         """
Laurent@1200:         Method that refresh the content displayed by Viewer
Laurent@1200:         Need to be overridden by inherited classes
Laurent@1200:         """
Laurent@1198:         pass
andrej@1730: 
Laurent@1198:     def SetHighlight(self, highlight):
Laurent@1200:         """
Laurent@1200:         Set Highlight type displayed in Viewer
Laurent@1200:         @return: True if highlight has changed
Laurent@1200:         """
Laurent@1200:         # Return immediately if highlight don't change
Laurent@1200:         if self.Highlight == highlight:
Laurent@1200:             return False
andrej@1730: 
Laurent@1200:         self.Highlight = highlight
Laurent@1200:         return True
andrej@1730: 
Laurent@1198:     def GetButtons(self):
Laurent@1200:         """
Laurent@1200:         Return list of buttons defined in Viewer
Laurent@1200:         @return: List of buttons
Laurent@1200:         """
Laurent@1198:         return self.Buttons
andrej@1730: 
Laurent@1200:     def IsOverButton(self, x, y):
Laurent@1200:         """
Laurent@1200:         Return if point is over one button of Viewer
Laurent@1200:         @param x: X coordinate of point
Laurent@1200:         @param y: Y coordinate of point
Laurent@1200:         @return: button where point is over
Laurent@1200:         """
Laurent@1198:         for button in self.GetButtons():
Laurent@1198:             if button.HitTest(x, y):
Laurent@1200:                 return button
Laurent@1200:         return None
andrej@1730: 
Laurent@1200:     def HandleButton(self, x, y):
Laurent@1200:         """
Laurent@1200:         Search for the button under point and if found execute associated
Laurent@1200:         callback
Laurent@1200:         @param x: X coordinate of point
Laurent@1200:         @param y: Y coordinate of point
Laurent@1200:         @return: True if a button was found and callback executed
Laurent@1200:         """
Laurent@1200:         button = self.IsOverButton(x, y)
Laurent@1200:         if button is None:
Laurent@1200:             return False
andrej@1730: 
Laurent@1200:         button.ProcessCallback()
Laurent@1200:         return True
andrej@1730: 
Laurent@1198:     def ShowButtons(self, show):
Laurent@1200:         """
Laurent@1200:         Set display state of buttons in Viewer
Laurent@1200:         @param show: Display state (True if buttons must be displayed)
Laurent@1200:         """
Laurent@1200:         # Change display of every buttons
Laurent@1198:         for button in self.Buttons:
Laurent@1200:             button.Show(show)
andrej@1730: 
Laurent@1200:         # Refresh button positions
Laurent@1200:         self.RefreshButtonsPosition()
Laurent@1200:         self.RefreshViewer()
andrej@1730: 
Laurent@1200:     def RefreshButtonsPosition(self):
Laurent@1200:         """
Laurent@1200:         Function that refresh buttons position in Viewer
Laurent@1200:         """
Laurent@1200:         # Get Viewer size
andrej@1847:         width, _height = self.GetSize()
andrej@1730: 
Laurent@1200:         # Buttons are align right so we calculate buttons positions in
Laurent@1200:         # reverse order
Laurent@1200:         buttons = self.Buttons[:]
Laurent@1200:         buttons.reverse()
andrej@1730: 
Laurent@1200:         # Position offset on x coordinate
Laurent@1200:         x_offset = 0
Laurent@1200:         for button in buttons:
andrej@1730:             # Buttons are stacked right, removing those that are not active
Laurent@1200:             if button.IsEnabled():
Laurent@1200:                 # Update button position according to button width and offset
Laurent@1200:                 # on x coordinate
andrej@1847:                 w, _h = button.GetSize()
Laurent@1200:                 button.SetPosition(width - 5 - w - x_offset, 5)
Laurent@1200:                 # Update offset on x coordinate
Laurent@1200:                 x_offset += w + 2
andrej@1730: 
Laurent@1198:     def DrawCommonElements(self, dc, buttons=None):
Laurent@1200:         """
Laurent@1200:         Function that draw common graphics for every Viewers
Laurent@1200:         @param dc: wx.DC object corresponding to Device context where drawing
Laurent@1200:         common graphics
Laurent@1200:         @param buttons: List of buttons to draw if different from default
Laurent@1200:         (default None)
Laurent@1200:         """
Laurent@1200:         # Get Viewer size
Laurent@1198:         width, height = self.GetSize()
andrej@1730: 
Laurent@1200:         # Set dc styling for drop before or drop after highlight
andrej@1823:         dc.SetPen(HIGHLIGHT['DROP_PEN'])
andrej@1823:         dc.SetBrush(HIGHLIGHT['DROP_BRUSH'])
andrej@1730: 
Laurent@1200:         # Draw line at upper side of Viewer if highlight is drop before
Laurent@1200:         if self.Highlight == HIGHLIGHT_BEFORE:
Laurent@1198:             dc.DrawLine(0, 1, width - 1, 1)
andrej@1730: 
Laurent@1200:         # Draw line at lower side of Viewer if highlight is drop before
Laurent@1200:         elif self.Highlight == HIGHLIGHT_AFTER:
Laurent@1198:             dc.DrawLine(0, height - 1, width - 1, height - 1)
andrej@1730: 
Laurent@1200:         # If no specific buttons are defined, get default buttons
Laurent@1198:         if buttons is None:
Laurent@1198:             buttons = self.Buttons
Laurent@1200:         # Draw buttons
Laurent@1198:         for button in buttons:
Laurent@1198:             button.Draw(dc)
andrej@1730: 
Laurent@1200:         # If graph dragging is processing
Laurent@1198:         if self.ParentWindow.IsDragging():
Laurent@1198:             destBBox = self.ParentWindow.GetDraggingAxesClippingRegion(self)
Laurent@1198:             srcPos = self.ParentWindow.GetDraggingAxesPosition(self)
Laurent@1198:             if destBBox.width > 0 and destBBox.height > 0:
Laurent@1198:                 srcPanel = self.ParentWindow.DraggingAxesPanel
Laurent@1198:                 srcBBox = srcPanel.GetAxesBoundingBox()
andrej@1730: 
Laurent@1200:                 srcX = srcBBox.x - (srcPos.x if destBBox.x == 0 else 0)
Laurent@1200:                 srcY = srcBBox.y - (srcPos.y if destBBox.y == 0 else 0)
andrej@1730: 
edouard@3798:                 agg_bitmap = srcPanel.get_renderer()
edouard@3798:                 srcBmp = wx.Bitmap.FromBufferRGBA(int(agg_bitmap.width), int(agg_bitmap.height),
edouard@3798:                                         agg_bitmap.buffer_rgba())
edouard@3798: 
Laurent@1198:                 srcDC = wx.MemoryDC()
Laurent@1198:                 srcDC.SelectObject(srcBmp)
andrej@1730: 
andrej@1730:                 dc.Blit(destBBox.x, destBBox.y,
andrej@1730:                         int(destBBox.width), int(destBBox.height),
Laurent@1198:                         srcDC, srcX, srcY)
andrej@1730: 
Laurent@1198:     def OnEnter(self, event):
Laurent@1200:         """
Laurent@1200:         Function called when entering Viewer
andrej@1730:         @param event: wx.MouseEvent
Laurent@1200:         """
Laurent@1200:         # Display buttons
Laurent@1198:         self.ShowButtons(True)
Laurent@1198:         event.Skip()
andrej@1730: 
Laurent@1198:     def OnLeave(self, event):
Laurent@1200:         """
Laurent@1200:         Function called when leaving Viewer
andrej@1730:         @param event: wx.MouseEvent
Laurent@1200:         """
Laurent@1209:         # Hide buttons
Laurent@1209:         self.ShowButtons(False)
Laurent@1198:         event.Skip()
andrej@1730: 
Laurent@1198:     def OnCloseButton(self):
Laurent@1200:         """
Laurent@1200:         Function called when Close button is pressed
Laurent@1200:         """
Laurent@1198:         wx.CallAfter(self.ParentWindow.DeleteValue, self)
andrej@1730: 
Laurent@1198:     def OnForceButton(self):
Laurent@1200:         """
Laurent@1200:         Function called when Force button is pressed
Laurent@1200:         """
kinsamanka@3750:         self.ForceValue(list(self.ItemsDict.values())[0])
andrej@1730: 
Laurent@1198:     def OnReleaseButton(self):
Laurent@1200:         """
Laurent@1200:         Function called when Release button is pressed
Laurent@1200:         """
kinsamanka@3750:         self.ReleaseValue(list(self.ItemsDict.values())[0])
andrej@1730: 
Laurent@1198:     def OnMouseDragging(self, x, y):
Laurent@1200:         """
Laurent@1200:         Function called when mouse is dragged over Viewer
Laurent@1200:         @param x: X coordinate of mouse pointer
Laurent@1200:         @param y: Y coordinate of mouse pointer
Laurent@1200:         """
Laurent@1198:         xw, yw = self.GetPosition()
Laurent@1200:         # Refresh highlight in Debug Variable Panel (highlight can be displayed
Laurent@1200:         # in another Viewer
Laurent@1198:         self.ParentWindow.RefreshHighlight(x + xw, y + yw)
andrej@1730: 
Laurent@1200:     def RefreshHighlight(self, x, y):
Laurent@1200:         """
Laurent@1200:         Function called by Debug Variable Panel asking Viewer to refresh
Laurent@1200:         highlight according to mouse position
Laurent@1200:         @param x: X coordinate of mouse pointer
Laurent@1200:         @param y: Y coordinate of mouse pointer
Laurent@1200:         """
Laurent@1200:         # Get Viewer size
andrej@1847:         _width, height = self.GetSize()
andrej@1730: 
Laurent@1200:         # Mouse is in the first half of Viewer
andrej@2437:         if y < height // 2:
Laurent@1200:             # If Viewer is the upper one, draw drop before highlight
Laurent@1198:             if self.ParentWindow.IsViewerFirst(self):
Laurent@1198:                 self.SetHighlight(HIGHLIGHT_BEFORE)
andrej@1730: 
Laurent@1200:             # Else draw drop after highlight in previous Viewer
Laurent@1198:             else:
Laurent@1198:                 self.SetHighlight(HIGHLIGHT_NONE)
Laurent@1198:                 self.ParentWindow.HighlightPreviousViewer(self)
andrej@1730: 
andrej@1730:         # Mouse is in the second half of Viewer, draw drop after highlight
Laurent@1198:         else:
Laurent@1198:             self.SetHighlight(HIGHLIGHT_AFTER)
andrej@1730: 
Laurent@1198:     def OnEraseBackground(self, event):
Laurent@1200:         """
Laurent@1200:         Function called when Viewer background is going to be erase
Laurent@1200:         @param event: wx.EraseEvent
Laurent@1200:         """
Laurent@1200:         # Prevent flicker on Windows
Laurent@1198:         pass
andrej@1730: 
Laurent@1198:     def OnResize(self, event):
Laurent@1200:         """
Laurent@1200:         Function called when Viewer size changed
Laurent@1200:         @param event: wx.ResizeEvent
Laurent@1200:         """
Laurent@1200:         # Refresh button positions
Laurent@1200:         self.RefreshButtonsPosition()
Laurent@1200:         self.ParentWindow.ForceRefresh()
Laurent@1198:         event.Skip()
andrej@1730: 
Laurent@1198:     def ForceValue(self, item):
Laurent@1200:         """
Laurent@1200:         Force value of item given
Laurent@1200:         @param item: Item to force value
Laurent@1200:         """
Laurent@1200:         # Check variable data type
Laurent@1198:         iec_path = item.GetVariable()
Laurent@1198:         iec_type = self.ParentWindow.GetDataType(iec_path)
Laurent@1200:         # Return immediately if not found
Laurent@1200:         if iec_type is None:
Laurent@1200:             return
andrej@1730: 
Laurent@1200:         # Open a dialog to enter varaible forced value
Laurent@1200:         dialog = ForceVariableDialog(self, iec_type, str(item.GetValue()))
Laurent@1200:         if dialog.ShowModal() == wx.ID_OK:
Laurent@1200:             self.ParentWindow.ForceDataValue(iec_path.upper(),
Laurent@1200:                                              dialog.GetValue())
andrej@1730: 
Laurent@1198:     def ReleaseValue(self, item):
Laurent@1200:         """
Laurent@1200:         Release value of item given
Laurent@1200:         @param item: Item to release value
Laurent@1200:         """
Laurent@1200:         self.ParentWindow.ReleaseDataValue(
andrej@1878:             item.GetVariable().upper())