Fix bug with two or more wires connected to one input. Now only one wire can be connected to one input, except BOOLean signals in LD and SFC. If user trying to connect wire with already connected input, wire highlight will become red.
Signed-off-by: Andrey Skvortsov <andrej.skvortzov@gmail.com>
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor
#based on the plcopen standard.
#
#Copyright (C) 2012: Edouard TISSERANT and Laurent BESSARD
#
#See COPYING file for copyrights details.
#
#This library is free software; you can redistribute it and/or
#modify it under the terms of the GNU General Public
#License as published by the Free Software Foundation; either
#version 2.1 of the License, or (at your option) any later version.
#
#This library is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
#General Public License for more details.
#
#You should have received a copy of the GNU General Public
#License along with this library; if not, write to the Free Software
#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from collections import OrderedDict
import wx
import matplotlib
import matplotlib.pyplot
from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap
from dialogs.ForceVariableDialog import ForceVariableDialog
# Viewer highlight types
[HIGHLIGHT_NONE,
HIGHLIGHT_BEFORE,
HIGHLIGHT_AFTER,
HIGHLIGHT_LEFT,
HIGHLIGHT_RIGHT,
HIGHLIGHT_RESIZE] = range(6)
# Viewer highlight styles
HIGHLIGHT_DROP_PEN = wx.Pen(wx.Colour(0, 128, 255))
HIGHLIGHT_DROP_BRUSH = wx.Brush(wx.Colour(0, 128, 255, 128))
HIGHLIGHT_RESIZE_PEN = wx.Pen(wx.Colour(200, 200, 200))
HIGHLIGHT_RESIZE_BRUSH = wx.Brush(wx.Colour(200, 200, 200))
#-------------------------------------------------------------------------------
# Base Debug Variable Viewer Class
#-------------------------------------------------------------------------------
"""
Class that implements a generic viewer that display a list of variable values
This class has to be inherited to effectively display variable values
"""
class DebugVariableViewer:
def __init__(self, window, items=[]):
"""
Constructor
@param window: Reference to the Debug Variable Panel
@param items: List of DebugVariableItem displayed by Viewer
"""
self.ParentWindow = window
self.ItemsDict = OrderedDict([(item.GetVariable(), item)
for item in items])
self.Items = self.ItemsDict.viewvalues()
# Variable storing current highlight displayed in Viewer
self.Highlight = HIGHLIGHT_NONE
# List of buttons
self.Buttons = []
def __del__(self):
"""
Destructor
"""
# Remove reference to Debug Variable Panel
self.ParentWindow = None
def GetIndex(self):
"""
Return position of Viewer in Debug Variable Panel
@return: Position of Viewer
"""
return self.ParentWindow.GetViewerIndex(self)
def GetItem(self, variable):
"""
Return item storing values of a variable
@param variable: Variable path
@return: Item storing values of this variable
"""
return self.ItemsDict.get(variable, None)
def GetItems(self):
"""
Return items displayed by Viewer
@return: List of items displayed in Viewer
"""
return self.ItemsDict.values()
def AddItem(self, item):
"""
Add an item to the list of items displayed by Viewer
@param item: Item to add to the list
"""
self.ItemsDict[item.GetVariable()] = item
def RemoveItem(self, item):
"""
Remove an item from the list of items displayed by Viewer
@param item: Item to remove from the list
"""
self.ItemsDict.pop(item.GetVariable(), None)
def ClearItems(self):
"""
Clear list of items displayed by Viewer
"""
# Unsubscribe every items of the list
for item in self.Items:
self.ParentWindow.RemoveDataConsumer(item)
# Clear list
self.ItemsDict.clear()
def ItemsIsEmpty(self):
"""
Return if list of items displayed by Viewer is empty
@return: True if list is empty
"""
return len(self.Items) == 0
def SubscribeAllDataConsumers(self):
"""
Function that unsubscribe and remove every item that store values of
a variable that doesn't exist in PLC anymore
"""
for item in self.ItemsDict.values()[:]:
iec_path = item.GetVariable()
# Check that variablepath exist in PLC
if self.ParentWindow.GetDataType(iec_path) is None:
# If not, unsubscribe and remove it
self.ParentWindow.RemoveDataConsumer(item)
self.RemoveItem(item)
else:
# If it exist, resubscribe and refresh data type
self.ParentWindow.AddDataConsumer(iec_path.upper(), item, True)
item.RefreshVariableType()
def ResetItemsData(self):
"""
Reset data stored in every items displayed in Viewer
"""
for item in self.Items:
item.ResetData()
def GetItemsMinCommonTick(self):
"""
Return the minimum tick common to all iems displayed in Viewer
@return: Minimum common tick between items
"""
return reduce(max, [item.GetData()[0, 0]
for item in self.Items
if len(item.GetData()) > 0], 0)
def RefreshViewer(self):
"""
Method that refresh the content displayed by Viewer
Need to be overridden by inherited classes
"""
pass
def SetHighlight(self, highlight):
"""
Set Highlight type displayed in Viewer
@return: True if highlight has changed
"""
# Return immediately if highlight don't change
if self.Highlight == highlight:
return False
self.Highlight = highlight
return True
def GetButtons(self):
"""
Return list of buttons defined in Viewer
@return: List of buttons
"""
return self.Buttons
def IsOverButton(self, x, y):
"""
Return if point is over one button of Viewer
@param x: X coordinate of point
@param y: Y coordinate of point
@return: button where point is over
"""
for button in self.GetButtons():
if button.HitTest(x, y):
return button
return None
def HandleButton(self, x, y):
"""
Search for the button under point and if found execute associated
callback
@param x: X coordinate of point
@param y: Y coordinate of point
@return: True if a button was found and callback executed
"""
button = self.IsOverButton(x, y)
if button is None:
return False
button.ProcessCallback()
return True
def ShowButtons(self, show):
"""
Set display state of buttons in Viewer
@param show: Display state (True if buttons must be displayed)
"""
# Change display of every buttons
for button in self.Buttons:
button.Show(show)
# Refresh button positions
self.RefreshButtonsPosition()
self.RefreshViewer()
def RefreshButtonsPosition(self):
"""
Function that refresh buttons position in Viewer
"""
# Get Viewer size
width, height = self.GetSize()
# Buttons are align right so we calculate buttons positions in
# reverse order
buttons = self.Buttons[:]
buttons.reverse()
# Position offset on x coordinate
x_offset = 0
for button in buttons:
# Buttons are stacked right, removing those that are not active
if button.IsEnabled():
# Update button position according to button width and offset
# on x coordinate
w, h = button.GetSize()
button.SetPosition(width - 5 - w - x_offset, 5)
# Update offset on x coordinate
x_offset += w + 2
def DrawCommonElements(self, dc, buttons=None):
"""
Function that draw common graphics for every Viewers
@param dc: wx.DC object corresponding to Device context where drawing
common graphics
@param buttons: List of buttons to draw if different from default
(default None)
"""
# Get Viewer size
width, height = self.GetSize()
# Set dc styling for drop before or drop after highlight
dc.SetPen(HIGHLIGHT_DROP_PEN)
dc.SetBrush(HIGHLIGHT_DROP_BRUSH)
# Draw line at upper side of Viewer if highlight is drop before
if self.Highlight == HIGHLIGHT_BEFORE:
dc.DrawLine(0, 1, width - 1, 1)
# Draw line at lower side of Viewer if highlight is drop before
elif self.Highlight == HIGHLIGHT_AFTER:
dc.DrawLine(0, height - 1, width - 1, height - 1)
# If no specific buttons are defined, get default buttons
if buttons is None:
buttons = self.Buttons
# Draw buttons
for button in buttons:
button.Draw(dc)
# If graph dragging is processing
if self.ParentWindow.IsDragging():
destBBox = self.ParentWindow.GetDraggingAxesClippingRegion(self)
srcPos = self.ParentWindow.GetDraggingAxesPosition(self)
if destBBox.width > 0 and destBBox.height > 0:
srcPanel = self.ParentWindow.DraggingAxesPanel
srcBBox = srcPanel.GetAxesBoundingBox()
srcX = srcBBox.x - (srcPos.x if destBBox.x == 0 else 0)
srcY = srcBBox.y - (srcPos.y if destBBox.y == 0 else 0)
srcBmp = _convert_agg_to_wx_bitmap(
srcPanel.get_renderer(), None)
srcDC = wx.MemoryDC()
srcDC.SelectObject(srcBmp)
dc.Blit(destBBox.x, destBBox.y,
int(destBBox.width), int(destBBox.height),
srcDC, srcX, srcY)
def OnEnter(self, event):
"""
Function called when entering Viewer
@param event: wx.MouseEvent
"""
# Display buttons
self.ShowButtons(True)
event.Skip()
def OnLeave(self, event):
"""
Function called when leaving Viewer
@param event: wx.MouseEvent
"""
# Hide buttons
self.ShowButtons(False)
event.Skip()
def OnCloseButton(self):
"""
Function called when Close button is pressed
"""
wx.CallAfter(self.ParentWindow.DeleteValue, self)
def OnForceButton(self):
"""
Function called when Force button is pressed
"""
self.ForceValue(self.ItemsDict.values()[0])
def OnReleaseButton(self):
"""
Function called when Release button is pressed
"""
self.ReleaseValue(self.ItemsDict.values()[0])
def OnMouseDragging(self, x, y):
"""
Function called when mouse is dragged over Viewer
@param x: X coordinate of mouse pointer
@param y: Y coordinate of mouse pointer
"""
xw, yw = self.GetPosition()
# Refresh highlight in Debug Variable Panel (highlight can be displayed
# in another Viewer
self.ParentWindow.RefreshHighlight(x + xw, y + yw)
def RefreshHighlight(self, x, y):
"""
Function called by Debug Variable Panel asking Viewer to refresh
highlight according to mouse position
@param x: X coordinate of mouse pointer
@param y: Y coordinate of mouse pointer
"""
# Get Viewer size
width, height = self.GetSize()
# Mouse is in the first half of Viewer
if y < height / 2:
# If Viewer is the upper one, draw drop before highlight
if self.ParentWindow.IsViewerFirst(self):
self.SetHighlight(HIGHLIGHT_BEFORE)
# Else draw drop after highlight in previous Viewer
else:
self.SetHighlight(HIGHLIGHT_NONE)
self.ParentWindow.HighlightPreviousViewer(self)
# Mouse is in the second half of Viewer, draw drop after highlight
else:
self.SetHighlight(HIGHLIGHT_AFTER)
def OnEraseBackground(self, event):
"""
Function called when Viewer background is going to be erase
@param event: wx.EraseEvent
"""
# Prevent flicker on Windows
pass
def OnResize(self, event):
"""
Function called when Viewer size changed
@param event: wx.ResizeEvent
"""
# Refresh button positions
self.RefreshButtonsPosition()
self.ParentWindow.ForceRefresh()
event.Skip()
def ForceValue(self, item):
"""
Force value of item given
@param item: Item to force value
"""
# Check variable data type
iec_path = item.GetVariable()
iec_type = self.ParentWindow.GetDataType(iec_path)
# Return immediately if not found
if iec_type is None:
return
# Open a dialog to enter varaible forced value
dialog = ForceVariableDialog(self, iec_type, str(item.GetValue()))
if dialog.ShowModal() == wx.ID_OK:
self.ParentWindow.ForceDataValue(iec_path.upper(),
dialog.GetValue())
def ReleaseValue(self, item):
"""
Release value of item given
@param item: Item to release value
"""
self.ParentWindow.ReleaseDataValue(
item.GetVariable().upper())