#!/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) 2007: 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
import wx
import wx.stc
from types import *
import re
#-------------------------------------------------------------------------------
# Textual programs Viewer class
#-------------------------------------------------------------------------------
NEWLINE = "\n"
NUMBERS = [str(i) for i in xrange(10)]
LETTERS = ['_']
for i in xrange(26):
LETTERS.append(chr(ord('a') + i))
LETTERS.append(chr(ord('A') + i))
[STC_PLC_WORD, STC_PLC_COMMENT, STC_PLC_NUMBER, STC_PLC_VARIABLE,
STC_PLC_FUNCTION, STC_PLC_JUMP, STC_PLC_ERROR] = range(7)
[SPACE, WORD, NUMBER, COMMENT] = range(4)
[ID_TEXTVIEWER,
] = [wx.NewId() for _init_ctrls in range(1)]
if wx.Platform == '__WXMSW__':
faces = { 'times': 'Times New Roman',
'mono' : 'Courier New',
'helv' : 'Arial',
'other': 'Comic Sans MS',
'size' : 10,
}
else:
faces = { 'times': 'Times',
'mono' : 'Courier',
'helv' : 'Helvetica',
'other': 'new century schoolbook',
'size' : 12,
}
re_texts = {}
re_texts["letter"] = "[A-Za-z]"
re_texts["digit"] = "[0-9]"
re_texts["identifier"] = "((?:%(letter)s|(?:_(?:%(letter)s|%(digit)s)))(?:_?(?:%(letter)s|%(digit)s))*)"%re_texts
IDENTIFIER_MODEL = re.compile(re_texts["identifier"])
LABEL_MODEL = re.compile("[ \t\n]%(identifier)s:[ \t\n]"%re_texts)
def GetCursorPos(old, new):
old_length = len(old)
new_length = len(new)
common_length = min(old_length, new_length)
i = 0
for i in xrange(common_length):
if old[i] != new[i]:
break
if old_length < new_length:
if common_length > 0 and old[i] != new[i]:
return i + new_length - old_length
else:
return i + new_length - old_length + 1
elif old_length > new_length or i < min(old_length, new_length) - 1:
if common_length > 0 and old[i] != new[i]:
return i
else:
return i + 1
else:
return None
class TextViewer(wx.stc.StyledTextCtrl):
if wx.VERSION < (2, 6, 0):
def Bind(self, event, function, id = None):
if id is not None:
event(self, id, function)
else:
event(self, function)
def __init__(self, parent, tagname, window, controler, debug = False, instancepath = ""):
wx.stc.StyledTextCtrl.__init__(self, parent, ID_TEXTVIEWER, size=wx.Size(0, 0), style=0)
self.CmdKeyAssign(ord('+'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMIN)
self.CmdKeyAssign(ord('-'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMOUT)
self.SetViewWhiteSpace(False)
self.SetLexer(wx.stc.STC_LEX_CONTAINER)
# Global default styles for all languages
self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d" % faces)
self.StyleClearAll() # Reset all to be like the default
self.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER, "back:#C0C0C0,size:%(size)d" % faces)
self.SetSelBackground(1, "#E0E0E0")
# Highlighting styles
self.StyleSetSpec(STC_PLC_WORD, "fore:#00007F,bold,size:%(size)d" % faces)
self.StyleSetSpec(STC_PLC_VARIABLE, "fore:#7F0000,size:%(size)d" % faces)
self.StyleSetSpec(STC_PLC_FUNCTION, "fore:#7F7F00,size:%(size)d" % faces)
self.StyleSetSpec(STC_PLC_COMMENT, "fore:#7F7F7F,size:%(size)d" % faces)
self.StyleSetSpec(STC_PLC_NUMBER, "fore:#007F7F,size:%(size)d" % faces)
self.StyleSetSpec(STC_PLC_JUMP, "fore:#007F00,size:%(size)d" % faces)
self.StyleSetSpec(STC_PLC_ERROR, "fore:#FF0000,back:#FFFF00,size:%(size)d" % faces)
# Indicators styles
self.IndicatorSetStyle(0, wx.stc.STC_INDIC_SQUIGGLE)
if window and controler:
self.IndicatorSetForeground(0, wx.RED)
else:
self.IndicatorSetForeground(0, wx.WHITE)
# Line numbers in the margin
self.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER)
self.SetMarginWidth(1, 50)
# Indentation size
self.SetTabWidth(2)
self.SetUseTabs(0)
self.Keywords = []
self.Variables = {}
self.Functions = []
self.Jumps = []
self.EnumeratedValues = []
self.DisableEvents = True
self.TextSyntax = "ST"
self.CurrentAction = None
self.TagName = tagname
self.Errors = []
self.Debug = debug
self.InstancePath = instancepath
self.StructElementsStack = []
self.ParentWindow = window
self.Controler = controler
self.SetModEventMask(wx.stc.STC_MOD_BEFOREINSERT|
wx.stc.STC_MOD_BEFOREDELETE|
wx.stc.STC_PERFORMED_USER)
self.Bind(wx.stc.EVT_STC_STYLENEEDED, self.OnStyleNeeded, id=ID_TEXTVIEWER)
if controler:
self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.Bind(wx.stc.EVT_STC_DO_DROP, self.OnDoDrop, id=ID_TEXTVIEWER)
self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
self.Bind(wx.stc.EVT_STC_MODIFIED, self.OnModification, id=ID_TEXTVIEWER)
def SetTagName(self, tagname):
self.TagName = tagname
def GetTagName(self):
return self.TagName
def GetInstancePath(self):
return self.InstancePath
def IsViewing(self, tagname):
if self.Debug:
return self.InstancePath == tagname
else:
return self.TagName == tagname
def IsDebugging(self):
return self.Debug
def SetMode(self, mode):
pass
def OnModification(self, event):
if not self.DisableEvents:
mod_type = event.GetModificationType()
if mod_type&wx.stc.STC_MOD_BEFOREINSERT:
if self.CurrentAction == None:
self.StartBuffering()
elif self.CurrentAction[0] != "Add" or self.CurrentAction[1] != event.GetPosition() - 1:
self.Controler.EndBuffering()
self.StartBuffering()
self.CurrentAction = ("Add", event.GetPosition())
wx.CallAfter(self.RefreshModel)
elif mod_type&wx.stc.STC_MOD_BEFOREDELETE:
if self.CurrentAction == None:
self.StartBuffering()
elif self.CurrentAction[0] != "Delete" or self.CurrentAction[1] != event.GetPosition() + 1:
self.Controler.EndBuffering()
self.StartBuffering()
self.CurrentAction = ("Delete", event.GetPosition())
wx.CallAfter(self.RefreshModel)
event.Skip()
def OnDoDrop(self, event):
try:
values = eval(event.GetDragText())
except:
values = event.GetDragText()
if isinstance(values, tuple):
message = None
if values[1] in ["functionBlock", "program", "debug"]:
event.SetDragText("")
elif values[1] == "function":
event.SetDragText(values[0])
elif values[1] == "location":
pou_name, pou_type = self.Controler.GetEditedElementType(self.TagName, self.Debug)
if len(values) > 2 and pou_type == "program":
var_name = values[3]
if var_name.upper() in [name.upper() for name in self.Controler.GetProjectPouNames(self.Debug)]:
message = _("\"%s\" pou already exists!")%var_name
elif var_name.upper() in [name.upper() for name in self.Controler.GetEditedElementVariables(self.TagName, self.Debug)]:
message = _("\"%s\" element for this pou already exists!")%var_name
else:
if values[2] is not None:
var_type = values[2]
else:
var_type = LOCATIONDATATYPES.get(values[0][2], ["BOOL"])[0]
self.Controler.AddEditedElementPouVar(self.TagName, var_type, var_name, values[0], values[4])
self.ParentWindow.RefreshVariablePanel(self.TagName)
self.RefreshVariableTree()
event.SetDragText(var_name)
else:
event.SetDragText("")
elif values[3] == self.TagName:
self.ResetBuffer()
event.SetDragText(values[0])
wx.CallAfter(self.RefreshModel)
else:
message = _("Variable don't belong to this POU!")
if message is not None:
dialog = wx.MessageDialog(self, message, _("Error"), wx.OK|wx.ICON_ERROR)
dialog.ShowModal()
dialog.Destroy()
event.SetDragText("")
event.Skip()
def SetTextSyntax(self, syntax):
self.TextSyntax = syntax
def SetKeywords(self, keywords):
self.Keywords = [keyword.upper() for keyword in keywords]
self.Colourise(0, -1)
def RefreshJumpList(self):
self.Jumps = [jump.upper() for jump in LABEL_MODEL.findall(self.GetText())]
self.Colourise(0, -1)
# Buffer the last model state
def RefreshBuffer(self):
self.Controler.BufferProject()
if self.ParentWindow:
self.ParentWindow.RefreshTitle()
self.ParentWindow.RefreshFileMenu()
self.ParentWindow.RefreshEditMenu()
def StartBuffering(self):
self.Controler.StartBuffering()
if self.ParentWindow:
self.ParentWindow.RefreshTitle()
self.ParentWindow.RefreshFileMenu()
self.ParentWindow.RefreshEditMenu()
def ResetBuffer(self):
if self.CurrentAction != None:
self.Controler.EndBuffering()
self.CurrentAction = None
def RefreshView(self):
self.ResetBuffer()
self.DisableEvents = True
old_cursor_pos = self.GetCurrentPos()
old_text = self.GetText()
new_text = self.Controler.GetEditedElementText(self.TagName, self.Debug)
self.SetText(new_text)
new_cursor_pos = GetCursorPos(old_text, new_text)
if new_cursor_pos != None:
self.GotoPos(new_cursor_pos)
else:
self.GotoPos(old_cursor_pos)
self.ScrollToColumn(0)
self.RefreshJumpList()
self.EmptyUndoBuffer()
self.DisableEvents = False
self.RefreshVariableTree()
self.Functions = []
for category in self.Controler.GetBlockTypes(self.TagName, self.Debug):
for blocktype in category["list"]:
if blocktype["type"] == "function" and blocktype["name"] not in self.Keywords and blocktype["name"] not in self.Variables.keys():
self.Functions.append(blocktype["name"].upper())
self.EnumeratedValues = []
for value in self.Controler.GetEnumeratedDataValues():
self.EnumeratedValues.append(value.upper())
self.Colourise(0, -1)
def RefreshVariableTree(self):
words = self.TagName.split("::")
self.Variables = self.GenerateVariableTree([(variable["Name"], variable["Type"], variable["Tree"]) for variable in self.Controler.GetEditedElementInterfaceVars(self.TagName, self.Debug)])
if self.Controler.GetEditedElementType(self.TagName, self.Debug)[1] == "function" or words[0] == "T" and self.TextSyntax == "IL":
self.Variables[words[-1].upper()] = {}
def GenerateVariableTree(self, list):
tree = {}
for var_name, var_type, (var_tree, var_dimension) in list:
tree[var_name.upper()] = self.GenerateVariableTree(var_tree)
return tree
def RefreshScaling(self, refresh=True):
pass
def IsValidVariable(self, name_list, var_tree):
if len(name_list) == 0:
return True
else:
sub_tree = var_tree.get(name_list[0].upper(), None)
if sub_tree is not None:
return self.IsValidVariable(name_list[1:], sub_tree)
return False
def OnStyleNeeded(self, event):
self.TextChanged = True
line = self.LineFromPosition(self.GetEndStyled())
if line == 0:
start_pos = last_styled_pos = 0
else:
start_pos = last_styled_pos = self.GetLineEndPosition(line - 1) + 1
end_pos = event.GetPosition()
self.StartStyling(start_pos, 0xff)
struct_elements = []
current_pos = last_styled_pos
state = SPACE
line = ""
word = ""
while current_pos < end_pos:
char = chr(self.GetCharAt(current_pos)).upper()
line += char
if char == NEWLINE:
if state == COMMENT:
self.SetStyling(current_pos - last_styled_pos + 1, STC_PLC_COMMENT)
elif state == NUMBER:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_NUMBER)
elif state == WORD:
if word in self.Keywords:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_WORD)
elif self.IsValidVariable(struct_elements + [word], self.Variables):
self.SetStyling(current_pos - last_styled_pos, STC_PLC_VARIABLE)
elif word in self.Functions:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_FUNCTION)
elif word in self.Jumps:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_JUMP)
elif word in self.EnumeratedValues:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_NUMBER)
else:
self.SetStyling(current_pos - last_styled_pos, 31)
if word != "]" and (self.GetCurrentPos() < last_styled_pos or self.GetCurrentPos() > current_pos):
self.StartStyling(last_styled_pos, wx.stc.STC_INDICS_MASK)
self.SetStyling(current_pos - last_styled_pos, wx.stc.STC_INDIC0_MASK)
self.StartStyling(current_pos, 0xff)
struct_elements = []
else:
self.SetStyling(current_pos - last_styled_pos, 31)
last_styled_pos = current_pos
state = SPACE
line = ""
elif line.endswith("(*") and state != COMMENT:
self.SetStyling(current_pos - last_styled_pos - 1, 31)
last_styled_pos = current_pos
if state == WORD:
struct_elements = []
state = COMMENT
elif state == COMMENT:
if line.endswith("*)"):
self.SetStyling(current_pos - last_styled_pos + 2, STC_PLC_COMMENT)
last_styled_pos = current_pos + 1
state = SPACE
elif char in LETTERS:
if state == NUMBER:
word = "#"
state = WORD
elif state == SPACE:
self.SetStyling(current_pos - last_styled_pos, 31)
word = char
last_styled_pos = current_pos
state = WORD
else:
word += char
elif char in NUMBERS or char == '.' and state != WORD:
if state == SPACE:
self.SetStyling(current_pos - last_styled_pos, 31)
last_styled_pos = current_pos
state = NUMBER
if state == WORD and char != '.':
word += char
else:
if state == WORD:
if word in self.Keywords:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_WORD)
elif self.IsValidVariable(struct_elements + [word], self.Variables):
self.SetStyling(current_pos - last_styled_pos, STC_PLC_VARIABLE)
elif word in self.Functions:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_FUNCTION)
elif word in self.Jumps:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_JUMP)
elif word in self.EnumeratedValues:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_NUMBER)
else:
self.SetStyling(current_pos - last_styled_pos, 31)
if word != "]" and (self.GetCurrentPos() < last_styled_pos or self.GetCurrentPos() > current_pos):
self.StartStyling(last_styled_pos, wx.stc.STC_INDICS_MASK)
self.SetStyling(current_pos - last_styled_pos, wx.stc.STC_INDIC0_MASK)
self.StartStyling(current_pos, 0xff)
if char == '.':
if word != "]":
struct_elements.append(word)
else:
if char == '[':
self.StructElementsStack.append(struct_elements + [word])
struct_elements = []
word = ""
last_styled_pos = current_pos
state = SPACE
elif state == NUMBER:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_NUMBER)
last_styled_pos = current_pos
state = SPACE
if char == ']':
if len(self.StructElementsStack) > 0:
struct_elements = self.StructElementsStack.pop()
word = char
state = WORD
current_pos += 1
if state == COMMENT:
self.SetStyling(current_pos - last_styled_pos + 2, STC_PLC_COMMENT)
elif state == NUMBER:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_NUMBER)
elif state == WORD:
if word in self.Keywords:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_WORD)
elif self.IsValidVariable(struct_elements + [word], self.Variables):
self.SetStyling(current_pos - last_styled_pos, STC_PLC_VARIABLE)
elif word in self.Functions:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_FUNCTION)
elif word in self.Jumps:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_JUMP)
elif word in self.EnumeratedValues:
self.SetStyling(current_pos - last_styled_pos, STC_PLC_NUMBER)
else:
self.SetStyling(current_pos - last_styled_pos, 31)
else:
self.SetStyling(current_pos - start_pos, 31)
self.ShowErrors(start_pos, end_pos)
event.Skip()
def Cut(self):
self.ResetBuffer()
self.CmdKeyExecute(wx.stc.STC_CMD_CUT)
self.RefreshModel()
self.RefreshBuffer()
def Copy(self):
self.CmdKeyExecute(wx.stc.STC_CMD_COPY)
def Paste(self):
self.ResetBuffer()
self.CmdKeyExecute(wx.stc.STC_CMD_PASTE)
self.RefreshModel()
self.RefreshBuffer()
def RefreshModel(self):
self.RefreshJumpList()
self.Controler.SetEditedElementText(self.TagName, self.GetText())
def OnKeyDown(self, event):
if self.CallTipActive():
self.CallTipCancel()
key = event.GetKeyCode()
# Code completion
if key == wx.WXK_SPACE and event.ControlDown():
line = self.GetCurrentLine()
if line == 0:
start_pos = 0
else:
start_pos = self.GetLineEndPosition(line - 1) + 1
end_pos = self.GetCurrentPos()
lineText = self.GetTextRange(start_pos, end_pos).replace("\t", " ")
words = lineText.split(" ")
words = [word for i, word in enumerate(words) if word != '' or i == len(words) - 1]
kw = []
if self.TextSyntax == "IL":
if len(words) == 1:
kw = self.Keywords
elif len(words) == 2:
if words[0].upper() in ["CAL", "CALC", "CALNC"]:
kw = self.Functions
elif words[0].upper() in ["JMP", "JMPC", "JMPNC"]:
kw = self.Jumps
else:
kw = self.Variables.keys()
else:
kw = self.Keywords + self.Variables.keys() + self.Functions
if len(kw) > 0:
if len(words[-1]) > 0:
kw = [keyword for keyword in kw if keyword.startswith(words[-1])]
kw.sort()
self.AutoCompSetIgnoreCase(True)
self.AutoCompShow(len(words[-1]), " ".join(kw))
else:
event.Skip()
def OnKillFocus(self, event):
self.AutoCompCancel()
event.Skip()
#-------------------------------------------------------------------------------
# Errors showing functions
#-------------------------------------------------------------------------------
def ClearErrors(self):
self.Errors = []
self.RefreshView()
def AddShownError(self, infos, start, end):
if infos[0] == "body":
self.Errors.append((infos[1], start, end))
def ShowErrors(self, start_pos, end_pos):
for indent, start, end in self.Errors:
if start[0] == 0:
error_start_pos = start[1] - indent
else:
error_start_pos = self.GetLineEndPosition(start[0] - 1) + start[1] - indent + 1
if end[0] == 0:
error_end_pos = end[1] - indent + 1
else:
error_end_pos = self.GetLineEndPosition(end[0] - 1) + end[1] - indent + 2
if start_pos <= error_start_pos <= end_pos or start_pos <= error_end_pos <= end_pos:
self.StartStyling(error_start_pos, 0xff)
self.SetStyling(error_end_pos - error_start_pos, STC_PLC_ERROR)