lbessard@27: #!/usr/bin/env python lbessard@27: # -*- coding: utf-8 -*- lbessard@27: lbessard@27: #This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor lbessard@27: #based on the plcopen standard. lbessard@27: # lbessard@58: #Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD lbessard@27: # lbessard@27: #See COPYING file for copyrights details. lbessard@27: # lbessard@27: #This library is free software; you can redistribute it and/or lbessard@27: #modify it under the terms of the GNU General Public lbessard@27: #License as published by the Free Software Foundation; either lbessard@27: #version 2.1 of the License, or (at your option) any later version. lbessard@27: # lbessard@27: #This library is distributed in the hope that it will be useful, lbessard@27: #but WITHOUT ANY WARRANTY; without even the implied warranty of lbessard@27: #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU lbessard@58: #General Public License for more details. lbessard@27: # lbessard@27: #You should have received a copy of the GNU General Public lbessard@27: #License along with this library; if not, write to the Free Software lbessard@27: #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA lbessard@27: lbessard@27: import wx lbessard@64: import wx.stc lbessard@56: from types import * lbessard@27: lbessard@27: import re lbessard@27: lbessard@27: #------------------------------------------------------------------------------- lbessard@27: # Textual programs Viewer class lbessard@27: #------------------------------------------------------------------------------- lbessard@27: lbessard@27: lbessard@27: NEWLINE = "\n" lbessard@27: NUMBERS = [str(i) for i in xrange(10)] lbessard@27: LETTERS = ['_'] lbessard@27: for i in xrange(26): lbessard@27: LETTERS.append(chr(ord('a') + i)) lbessard@27: LETTERS.append(chr(ord('A') + i)) lbessard@27: lbessard@64: [STC_PLC_WORD, STC_PLC_COMMENT, STC_PLC_NUMBER, STC_PLC_VARIABLE, lbessard@64: STC_PLC_FUNCTION, STC_PLC_JUMP] = range(6) lbessard@27: [SPACE, WORD, NUMBER, COMMENT] = range(4) lbessard@27: lbessard@64: [ID_TEXTVIEWER, lbessard@27: ] = [wx.NewId() for _init_ctrls in range(1)] lbessard@27: lbessard@27: if wx.Platform == '__WXMSW__': lbessard@27: faces = { 'times': 'Times New Roman', lbessard@27: 'mono' : 'Courier New', lbessard@27: 'helv' : 'Arial', lbessard@27: 'other': 'Comic Sans MS', lbessard@27: 'size' : 10, lbessard@27: } lbessard@27: else: lbessard@27: faces = { 'times': 'Times', lbessard@27: 'mono' : 'Courier', lbessard@27: 'helv' : 'Helvetica', lbessard@27: 'other': 'new century schoolbook', lbessard@27: 'size' : 12, lbessard@27: } lbessard@27: re_texts = {} lbessard@27: re_texts["letter"] = "[A-Za-z]" lbessard@27: re_texts["digit"] = "[0-9]" lbessard@27: re_texts["identifier"] = "((?:%(letter)s|(?:_(?:%(letter)s|%(digit)s)))(?:_?(?:%(letter)s|%(digit)s))*)"%re_texts lbessard@27: IDENTIFIER_MODEL = re.compile(re_texts["identifier"]) lbessard@27: LABEL_MODEL = re.compile("[ \t\n]%(identifier)s:[ \t\n]"%re_texts) lbessard@27: lbessard@56: def GetCursorPos(old, new): lbessard@56: old_length = len(old) lbessard@56: new_length = len(new) lbessard@56: common_length = min(old_length, new_length) lbessard@56: i = 0 lbessard@56: for i in xrange(common_length): lbessard@56: if old[i] != new[i]: lbessard@56: break lbessard@56: if old_length < new_length: lbessard@56: if common_length > 0 and old[i] != new[i]: lbessard@56: return i + new_length - old_length lbessard@56: else: lbessard@56: return i + new_length - old_length + 1 lbessard@56: elif old_length > new_length or i < min(old_length, new_length) - 1: lbessard@56: if common_length > 0 and old[i] != new[i]: lbessard@56: return i lbessard@56: else: lbessard@56: return i + 1 lbessard@56: else: lbessard@56: return None lbessard@56: lbessard@121: if wx.VERSION >= (2, 8, 0): lbessard@121: import wx.aui lbessard@121: lbessard@121: class TextMDIViewer(wx.aui.AuiMDIChildFrame): lbessard@121: def __init__(self, parent, tagname, window, controler): lbessard@121: wx.aui.AuiMDIChildFrame.__init__(self, parent, -1, title = "") lbessard@121: lbessard@121: sizer = wx.BoxSizer(wx.HORIZONTAL) lbessard@121: lbessard@121: self.Viewer = TextViewer(self, tagname, window, controler) lbessard@121: lbessard@121: sizer.AddWindow(self.Viewer, 1, border=0, flag=wx.GROW) lbessard@121: lbessard@121: self.SetSizer(sizer) lbessard@121: lbessard@121: def GetViewer(self): lbessard@121: return self.Viewer lbessard@121: lbessard@64: class TextViewer(wx.stc.StyledTextCtrl): lbessard@27: lbessard@113: if wx.VERSION < (2, 6, 0): lbessard@113: def Bind(self, event, function, id = None): lbessard@113: if id is not None: lbessard@113: event(self, id, function) lbessard@113: else: lbessard@113: event(self, function) lbessard@113: lbessard@121: def __init__(self, parent, tagname, window, controler): lbessard@80: wx.stc.StyledTextCtrl.__init__(self, parent, ID_TEXTVIEWER, size=wx.Size(0, 0), style=0) lbessard@64: lbessard@64: self.CmdKeyAssign(ord('+'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMIN) lbessard@64: self.CmdKeyAssign(ord('-'), wx.stc.STC_SCMOD_CTRL, wx.stc.STC_CMD_ZOOMOUT) lbessard@27: lbessard@27: self.SetViewWhiteSpace(False) lbessard@27: lbessard@64: self.SetLexer(wx.stc.STC_LEX_CONTAINER) lbessard@27: lbessard@27: # Global default styles for all languages lbessard@64: self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, "face:%(mono)s,size:%(size)d" % faces) lbessard@27: self.StyleClearAll() # Reset all to be like the default lbessard@27: lbessard@64: self.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER, "back:#C0C0C0,size:%(size)d" % faces) lbessard@27: self.SetSelBackground(1, "#E0E0E0") lbessard@27: lbessard@27: # Highlighting styles lbessard@64: self.StyleSetSpec(STC_PLC_WORD, "fore:#00007F,bold,size:%(size)d" % faces) lbessard@64: self.StyleSetSpec(STC_PLC_VARIABLE, "fore:#7F0000,size:%(size)d" % faces) lbessard@64: self.StyleSetSpec(STC_PLC_FUNCTION, "fore:#7F7F00,size:%(size)d" % faces) lbessard@64: self.StyleSetSpec(STC_PLC_COMMENT, "fore:#7F7F7F,size:%(size)d" % faces) lbessard@64: self.StyleSetSpec(STC_PLC_NUMBER, "fore:#007F7F,size:%(size)d" % faces) lbessard@64: self.StyleSetSpec(STC_PLC_JUMP, "fore:#007F00,size:%(size)d" % faces) lbessard@27: lbessard@27: # Indicators styles lbessard@64: self.IndicatorSetStyle(0, wx.stc.STC_INDIC_SQUIGGLE) lbessard@90: if window and controler: lbessard@90: self.IndicatorSetForeground(0, wx.RED) lbessard@90: else: lbessard@90: self.IndicatorSetForeground(0, wx.WHITE) lbessard@27: lbessard@27: # Line numbers in the margin lbessard@64: self.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER) lbessard@27: self.SetMarginWidth(1, 50) lbessard@27: lbessard@27: # Indentation size lbessard@27: self.SetTabWidth(2) lbessard@27: self.SetUseTabs(0) lbessard@27: lbessard@27: self.Keywords = [] lbessard@27: self.Variables = [] lbessard@27: self.Functions = [] lbessard@27: self.Jumps = [] lbessard@125: self.EnumeratedValues = [] lbessard@56: self.DisableEvents = True lbessard@27: self.TextSyntax = "ST" lbessard@56: self.CurrentAction = None lbessard@121: self.TagName = tagname lbessard@56: lbessard@90: self.ParentWindow = window lbessard@27: self.Controler = controler lbessard@27: lbessard@64: self.SetModEventMask(wx.stc.STC_MOD_BEFOREINSERT|wx.stc.STC_MOD_BEFOREDELETE) lbessard@64: etisserant@79: self.Bind(wx.stc.EVT_STC_STYLENEEDED, self.OnStyleNeeded, id=ID_TEXTVIEWER) lbessard@116: if controler: etisserant@73: self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) etisserant@73: self.Bind(wx.stc.EVT_STC_DO_DROP, self.OnDoDrop, id=ID_TEXTVIEWER) etisserant@73: self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) etisserant@73: self.Bind(wx.stc.EVT_STC_MODIFIED, self.OnModification, id=ID_TEXTVIEWER) lbessard@56: lbessard@121: def SetTagName(self, tagname): lbessard@121: self.TagName = tagname lbessard@121: lbessard@121: def GetTagName(self): lbessard@121: return self.TagName lbessard@121: lbessard@121: def IsViewing(self, tagname): lbessard@121: return self.TagName == tagname lbessard@121: lbessard@121: def SetMode(self, mode): lbessard@121: pass lbessard@121: lbessard@56: def OnModification(self, event): lbessard@56: if not self.DisableEvents: lbessard@56: mod_type = event.GetModificationType() lbessard@64: if not (mod_type&wx.stc.STC_PERFORMED_UNDO or mod_type&wx.stc.STC_PERFORMED_REDO): lbessard@64: if mod_type&wx.stc.STC_MOD_BEFOREINSERT: lbessard@56: if self.CurrentAction == None: lbessard@56: self.StartBuffering() lbessard@56: elif self.CurrentAction[0] != "Add" or self.CurrentAction[1] != event.GetPosition() - 1: lbessard@56: self.Controler.EndBuffering() lbessard@56: self.StartBuffering() lbessard@56: self.CurrentAction = ("Add", event.GetPosition()) lbessard@64: elif mod_type&wx.stc.STC_MOD_BEFOREDELETE: lbessard@56: if self.CurrentAction == None: lbessard@56: self.StartBuffering() lbessard@56: elif self.CurrentAction[0] != "Delete" or self.CurrentAction[1] != event.GetPosition() + 1: lbessard@56: self.Controler.EndBuffering() lbessard@56: self.StartBuffering() lbessard@56: self.CurrentAction = ("Delete", event.GetPosition()) lbessard@56: event.Skip() lbessard@27: lbessard@47: def OnDoDrop(self, event): lbessard@50: try: lbessard@50: values = eval(event.GetDragText()) lbessard@50: except: lbessard@50: values = event.GetDragText() lbessard@47: if isinstance(values, tuple): lbessard@53: if values[1] in ["functionBlock", "program", "location"]: lbessard@47: event.SetDragText("") lbessard@122: elif values[1] == "function": lbessard@122: event.SetDragText(values[0]) lbessard@121: elif values[1] != "location": lbessard@121: if values[3] == self.TagName: lbessard@136: self.ResetBuffer() lbessard@121: event.SetDragText(values[0]) lbessard@136: wx.CallAfter(self.RefreshModel) lbessard@121: else: lbessard@121: event.SetDragText("") lbessard@121: message = wx.MessageDialog(self.ParentWindow, "Variable don't belong to this POU!", "Error", wx.OK|wx.ICON_ERROR) lbessard@121: message.ShowModal() lbessard@121: message.Destroy() lbessard@47: event.Skip() lbessard@47: lbessard@27: def SetTextSyntax(self, syntax): lbessard@27: self.TextSyntax = syntax lbessard@27: lbessard@27: def SetKeywords(self, keywords): lbessard@27: self.Keywords = [keyword.upper() for keyword in keywords] lbessard@27: self.Colourise(0, -1) lbessard@27: lbessard@27: def RefreshJumpList(self): lbessard@27: self.Jumps = [jump.upper() for jump in LABEL_MODEL.findall(self.GetText())] lbessard@27: self.Colourise(0, -1) lbessard@27: lbessard@56: # Buffer the last model state lbessard@56: def RefreshBuffer(self): lbessard@56: self.Controler.BufferProject() lbessard@116: if self.ParentWindow: lbessard@116: self.ParentWindow.RefreshTitle() lbessard@116: self.ParentWindow.RefreshEditMenu() lbessard@56: lbessard@56: def StartBuffering(self): lbessard@56: self.Controler.StartBuffering() lbessard@116: if self.ParentWindow: lbessard@116: self.ParentWindow.RefreshTitle() lbessard@116: self.ParentWindow.RefreshEditMenu() lbessard@56: lbessard@56: def ResetBuffer(self): lbessard@56: if self.CurrentAction != None: lbessard@56: self.Controler.EndBuffering() lbessard@56: self.CurrentAction = None lbessard@56: lbessard@27: def RefreshView(self): lbessard@56: self.ResetBuffer() lbessard@56: self.DisableEvents = True lbessard@56: old_cursor_pos = self.GetCurrentPos() lbessard@56: old_text = self.GetText() lbessard@121: new_text = self.Controler.GetEditedElementText(self.TagName) lbessard@56: self.SetText(new_text) lbessard@56: new_cursor_pos = GetCursorPos(old_text, new_text) lbessard@56: if new_cursor_pos != None: lbessard@80: self.GotoPos(new_cursor_pos) lbessard@80: else: lbessard@80: self.GotoPos(old_cursor_pos) lbessard@80: self.ScrollToColumn(0) lbessard@27: self.RefreshJumpList() lbessard@56: self.EmptyUndoBuffer() lbessard@56: self.DisableEvents = False lbessard@121: lbessard@121: words = self.TagName.split("::") lbessard@121: self.Variables = [variable["Name"].upper() for variable in self.Controler.GetEditedElementInterfaceVars(self.TagName)] lbessard@121: if self.Controler.GetEditedElementType(self.TagName)[1] == "function" or words[0] == "T" and self.TextSyntax == "IL": lbessard@121: self.Variables.append(words[-1].upper()) lbessard@121: lbessard@121: self.Functions = [] lbessard@121: for category in self.Controler.GetBlockTypes(self.TagName): lbessard@121: for blocktype in category["list"]: lbessard@121: if blocktype["type"] == "function" and blocktype["name"] not in self.Keywords and blocktype["name"] not in self.Variables: lbessard@121: self.Functions.append(blocktype["name"].upper()) lbessard@121: lbessard@125: self.EnumeratedValues = [] lbessard@125: for value in self.Controler.GetEnumeratedDataValues(): lbessard@125: self.EnumeratedValues.append(value.upper()) lbessard@125: lbessard@121: self.Colourise(0, -1) lbessard@98: lbessard@145: def RefreshScaling(self, refresh=True): lbessard@145: pass lbessard@145: lbessard@27: def OnStyleNeeded(self, event): lbessard@27: self.TextChanged = True lbessard@27: line = self.LineFromPosition(self.GetEndStyled()) lbessard@27: if line == 0: lbessard@27: start_pos = 0 lbessard@27: else: lbessard@27: start_pos = self.GetLineEndPosition(line - 1) + 1 lbessard@27: end_pos = event.GetPosition() lbessard@27: self.StartStyling(start_pos, 0xff) lbessard@27: lbessard@27: i = start_pos lbessard@27: state = SPACE lbessard@27: line = "" lbessard@27: word = "" lbessard@27: while i < end_pos: lbessard@27: char = chr(self.GetCharAt(i)).upper() lbessard@27: line += char lbessard@27: if char == NEWLINE: lbessard@27: if state == COMMENT: lbessard@64: self.SetStyling(i - start_pos + 1, STC_PLC_COMMENT) lbessard@27: elif state == NUMBER: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_NUMBER) lbessard@27: elif state == WORD: lbessard@27: if word in self.Keywords: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_WORD) lbessard@27: elif word in self.Variables: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_VARIABLE) lbessard@27: elif word in self.Functions: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_FUNCTION) lbessard@27: elif word in self.Jumps: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_JUMP) lbessard@125: elif word in self.EnumeratedValues: lbessard@125: self.SetStyling(i - start_pos, STC_PLC_NUMBER) lbessard@27: else: lbessard@27: self.SetStyling(i - start_pos, 31) lbessard@27: if self.GetCurrentPos() < start_pos or self.GetCurrentPos() > i: lbessard@64: self.StartStyling(start_pos, wx.stc.STC_INDICS_MASK) lbessard@64: self.SetStyling(i - start_pos, wx.stc.STC_INDIC0_MASK) lbessard@27: self.StartStyling(i, 0xff) lbessard@27: else: lbessard@27: self.SetStyling(i - start_pos, 31) lbessard@27: start_pos = i lbessard@27: state = SPACE lbessard@27: line = "" lbessard@27: elif line.endswith("(*") and state != COMMENT: lbessard@27: self.SetStyling(i - start_pos - 1, 31) lbessard@27: start_pos = i lbessard@27: state = COMMENT lbessard@27: elif state == COMMENT: lbessard@27: if line.endswith("*)"): lbessard@64: self.SetStyling(i - start_pos + 2, STC_PLC_COMMENT) lbessard@27: start_pos = i + 1 lbessard@27: state = SPACE lbessard@27: elif char in LETTERS: lbessard@27: if state == NUMBER: lbessard@27: word = "#" lbessard@27: state = WORD lbessard@27: elif state == SPACE: lbessard@27: self.SetStyling(i - start_pos, 31) lbessard@27: word = char lbessard@27: start_pos = i lbessard@27: state = WORD lbessard@27: else: lbessard@27: word += char lbessard@27: elif char in NUMBERS or char == '.' and state != WORD: lbessard@27: if state == SPACE: lbessard@27: self.SetStyling(i - start_pos, 31) lbessard@27: start_pos = i lbessard@27: state = NUMBER lbessard@27: if state == WORD and char != '.': lbessard@27: word += char lbessard@27: else: lbessard@27: if state == WORD: lbessard@27: if word in self.Keywords: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_WORD) lbessard@27: elif word in self.Variables: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_VARIABLE) lbessard@27: elif word in self.Functions: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_FUNCTION) lbessard@27: elif word in self.Jumps: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_JUMP) lbessard@125: elif word in self.EnumeratedValues: lbessard@125: self.SetStyling(i - start_pos, STC_PLC_NUMBER) lbessard@27: else: lbessard@27: self.SetStyling(i - start_pos, 31) lbessard@27: if self.GetCurrentPos() < start_pos or self.GetCurrentPos() > i: lbessard@64: self.StartStyling(start_pos, wx.stc.STC_INDICS_MASK) lbessard@64: self.SetStyling(i - start_pos, wx.stc.STC_INDIC0_MASK) lbessard@27: self.StartStyling(i, 0xff) lbessard@27: word = "" lbessard@27: start_pos = i lbessard@27: state = SPACE lbessard@27: elif state == NUMBER: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_NUMBER) lbessard@27: start_pos = i lbessard@27: state = SPACE lbessard@27: i += 1 lbessard@27: if state == COMMENT: lbessard@64: self.SetStyling(i - start_pos + 2, STC_PLC_COMMENT) lbessard@27: elif state == NUMBER: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_NUMBER) lbessard@27: elif state == WORD: lbessard@27: if word in self.Keywords: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_WORD) lbessard@27: elif word in self.Variables: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_VARIABLE) lbessard@27: elif word in self.Functions: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_FUNCTION) lbessard@27: elif word in self.Jumps: lbessard@64: self.SetStyling(i - start_pos, STC_PLC_JUMP) lbessard@125: elif word in self.EnumeratedValues: lbessard@125: self.SetStyling(i - start_pos, STC_PLC_NUMBER) lbessard@27: else: lbessard@27: self.SetStyling(i - start_pos, 31) lbessard@27: else: lbessard@27: self.SetStyling(i - start_pos, 31) lbessard@27: event.Skip() lbessard@27: lbessard@27: def Cut(self): lbessard@56: self.ResetBuffer() lbessard@64: self.CmdKeyExecute(wx.stc.STC_CMD_CUT) lbessard@136: self.RefreshModel() lbessard@56: self.RefreshBuffer() lbessard@56: lbessard@27: def Copy(self): lbessard@64: self.CmdKeyExecute(wx.stc.STC_CMD_COPY) lbessard@27: lbessard@27: def Paste(self): lbessard@56: self.ResetBuffer() lbessard@64: self.CmdKeyExecute(wx.stc.STC_CMD_PASTE) lbessard@136: self.RefreshModel() lbessard@56: self.RefreshBuffer() lbessard@27: lbessard@27: def RefreshModel(self): lbessard@136: self.RefreshJumpList() lbessard@136: self.Controler.SetEditedElementText(self.TagName, self.GetText()) lbessard@27: lbessard@27: def OnKeyDown(self, event): lbessard@27: if self.CallTipActive(): lbessard@27: self.CallTipCancel() lbessard@121: key = event.GetKeyCode() lbessard@27: lbessard@27: # Code completion lbessard@64: if key == wx.WXK_SPACE and event.ControlDown(): lbessard@27: lbessard@27: line = self.GetCurrentLine() lbessard@27: if line == 0: lbessard@27: start_pos = 0 lbessard@27: else: lbessard@27: start_pos = self.GetLineEndPosition(line - 1) + 1 lbessard@27: end_pos = self.GetCurrentPos() lbessard@27: lbessard@27: lineText = self.GetTextRange(start_pos, end_pos).replace("\t", " ") lbessard@27: words = lineText.split(" ") lbessard@27: words = [word for i, word in enumerate(words) if word != '' or i == len(words) - 1] lbessard@27: lbessard@27: kw = [] lbessard@27: lbessard@27: if self.TextSyntax == "IL": lbessard@27: if len(words) == 1: lbessard@27: kw = self.Keywords lbessard@27: elif len(words) == 2: lbessard@27: if words[0].upper() in ["CAL", "CALC", "CALNC"]: lbessard@27: kw = self.Functions lbessard@27: elif words[0].upper() in ["JMP", "JMPC", "JMPNC"]: lbessard@27: kw = self.Jumps lbessard@27: else: lbessard@27: kw = self.Variables lbessard@27: else: lbessard@27: kw = self.Keywords + self.Variables + self.Functions lbessard@27: if len(kw) > 0: lbessard@27: kw.sort() lbessard@27: self.AutoCompSetIgnoreCase(True) lbessard@27: self.AutoCompShow(len(words[-1]), " ".join(kw)) lbessard@27: else: lbessard@64: wx.CallAfter(self.RefreshModel) lbessard@27: event.Skip() lbessard@27: lbessard@27: def OnKillFocus(self, event): lbessard@27: self.AutoCompCancel() lbessard@27: event.Skip() lbessard@27: