# HG changeset patch # User laurent # Date 1326815040 -3600 # Node ID b7062a7018ec81929b49d178482baa1fb9f6f698 # Parent efedc9d06a599e45539dfd0410e831ed23186b9f Adding support for defining a description for a user POU type, and displaying this information in library tree. Adding support for displaying a tooltip containing the informations of a function block when passing mouse over it in a graphic editor. diff -r efedc9d06a59 -r b7062a7018ec PLCControler.py --- a/PLCControler.py Thu Jan 12 17:04:22 2012 +0100 +++ b/PLCControler.py Tue Jan 17 16:44:00 2012 +0100 @@ -809,6 +809,27 @@ resource.setname(new_name) self.BufferProject() + # Return the description of the pou given by its name + def GetPouDescription(self, name, debug = False): + project = self.GetProject(debug) + if project is not None: + # Found the pou correponding to name and return its type + pou = project.getpou(name) + if pou is not None: + return pou.getdescription() + return "" + + # Return the description of the pou given by its name + def SetPouDescription(self, name, description, debug = False): + project = self.GetProject(debug) + if project is not None: + # Found the pou correponding to name and return its type + pou = project.getpou(name) + if pou is not None: + pou.setdescription(description) + project.RefreshCustomBlockTypes() + self.BufferProject() + # Return the type of the pou given by its name def GetPouType(self, name, debug = False): project = self.GetProject(debug) diff -r efedc9d06a59 -r b7062a7018ec PLCOpenEditor.py --- a/PLCOpenEditor.py Thu Jan 12 17:04:22 2012 +0100 +++ b/PLCOpenEditor.py Tue Jan 17 16:44:00 2012 +0100 @@ -1939,6 +1939,12 @@ def RefreshLibraryTree(self): if self.Controler is not None: to_delete = [] + selected_name = None + selected = self.LibraryTree.GetSelection() + if selected.IsOk(): + selected_pydata = self.LibraryTree.GetPyData(selected) + if selected_pydata is not None and selected_pydata["type"] != CATEGORY: + selected_name = self.LibraryTree.GetItemText(selected) blocktypes = self.Controler.GetBlockTypes() root = self.LibraryTree.GetRootItem() if not root.IsOk(): @@ -1968,6 +1974,10 @@ else: self.LibraryTree.SetItemText(blocktype_item, blocktype["name"]) self.LibraryTree.SetPyData(blocktype_item, {"type" : BLOCK, "block_type" : blocktype["type"], "inputs" : tuple([type for name, type, modifier in blocktype["inputs"]])}) + if selected_name == blocktype["name"]: + self.LibraryTree.SelectItem(blocktype_item) + comment = blocktype["comment"] + self.LibraryComment.SetValue(_(comment) + blocktype.get("usage", "")) blocktype_item, category_cookie = self.LibraryTree.GetNextChild(category_item, category_cookie) while blocktype_item.IsOk(): to_delete.append(blocktype_item) diff -r efedc9d06a59 -r b7062a7018ec Viewer.py --- a/Viewer.py Thu Jan 12 17:04:22 2012 +0100 +++ b/Viewer.py Tue Jan 17 16:44:00 2012 +0100 @@ -1666,22 +1666,20 @@ self.RefreshVisibleElements() else: if not event.Dragging(): - if self.Debug: + highlighted = self.FindElement(event) + if self.HighlightedElement is not None and self.HighlightedElement != highlighted: + self.HighlightedElement.ClearToolTip() + self.HighlightedElement.SetHighlighted(False) + self.HighlightedElement = None + if highlighted is not None: tooltip_pos = self.Editor.ClientToScreen(event.GetPosition()) tooltip_pos.x += 10 tooltip_pos.y += 10 - highlighted = self.FindElement(event) - if self.HighlightedElement is not None and self.HighlightedElement != highlighted: - if self.Debug and isinstance(self.HighlightedElement, Wire): - self.HighlightedElement.ClearToolTip() - self.HighlightedElement.SetHighlighted(False) - self.HighlightedElement = None - if highlighted is not None and self.HighlightedElement != highlighted: - if self.Debug and isinstance(highlighted, Wire): + if self.HighlightedElement != highlighted: highlighted.CreateToolTip(tooltip_pos) - highlighted.SetHighlighted(True) - elif self.Debug and highlighted is not None and isinstance(highlighted, Wire): - highlighted.MoveToolTip(tooltip_pos) + highlighted.SetHighlighted(True) + else: + highlighted.MoveToolTip(tooltip_pos) self.HighlightedElement = highlighted if self.rubberBand.IsShown(): self.rubberBand.OnMotion(event, dc, self.Scaling) @@ -1722,8 +1720,7 @@ if self.SelectedElement is not None and self.SelectedElement.GetDragging(): event.Skip() elif self.HighlightedElement is not None: - if isinstance(self.HighlightedElement, Wire): - self.HighlightedElement.ClearToolTip() + self.HighlightedElement.ClearToolTip() self.HighlightedElement.SetHighlighted(False) self.HighlightedElement = None event.Skip() diff -r efedc9d06a59 -r b7062a7018ec controls/VariablePanel.py --- a/controls/VariablePanel.py Thu Jan 12 17:04:22 2012 +0100 +++ b/controls/VariablePanel.py Tue Jan 17 16:44:00 2012 +0100 @@ -29,7 +29,7 @@ from types import TupleType, StringType, UnicodeType from plcopen.structures import LOCATIONDATATYPES, TestIdentifier, IEC_KEYWORDS -from graphics.GraphicCommons import REFRESH_HIGHLIGHT_PERIOD +from graphics.GraphicCommons import REFRESH_HIGHLIGHT_PERIOD, ERROR_HIGHLIGHT from dialogs.ArrayTypeDialog import ArrayTypeDialog from CustomGrid import CustomGrid from CustomTable import CustomTable @@ -86,7 +86,7 @@ "External": lambda x: {"Constant": "Constant"}.get(x, "") } -LOCATION_MODEL = re.compile("((?:%[IQM](?:\*|(?:[XBWLD]?[0-9](?:\.[0-9])*)))?)$") +LOCATION_MODEL = re.compile("((?:%[IQM](?:\*|(?:[XBWLD]?[0-9]+(?:\.[0-9]+)*)))?)$") class VariableTable(CustomTable): @@ -198,7 +198,10 @@ grid.SetCellEditor(row, col, editor) grid.SetCellRenderer(row, col, renderer) - highlight_colours = row_highlights.get(colname.lower(), [(wx.WHITE, wx.BLACK)])[-1] + if colname == "Location" and LOCATION_MODEL.match(self.GetValueByName(row, "Location")) is None: + highlight_colours = ERROR_HIGHLIGHT + else: + highlight_colours = row_highlights.get(colname.lower(), [(wx.WHITE, wx.BLACK)])[-1] grid.SetCellBackgroundColour(row, col, highlight_colours[0]) grid.SetCellTextColour(row, col, highlight_colours[1]) self.ResizeRow(grid, row) @@ -306,12 +309,13 @@ message.Destroy() [ID_VARIABLEEDITORPANEL, ID_VARIABLEEDITORPANELVARIABLESGRID, - ID_VARIABLEEDITORCONTROLPANEL, ID_VARIABLEEDITORPANELRETURNTYPE, + ID_VARIABLEEDITORCONTROLPANEL, ID_VARIABLEEDITORPANELRETURNTYPELABEL, + ID_VARIABLEEDITORPANELRETURNTYPE, ID_VARIABLEEDITORPANELDESCRIPTIONLABEL, + ID_VARIABLEEDITORPANELDESCRIPTION, ID_VARIABLEEDITORPANELCLASSFILTERLABEL, ID_VARIABLEEDITORPANELCLASSFILTER, ID_VARIABLEEDITORPANELADDBUTTON, ID_VARIABLEEDITORPANELDELETEBUTTON, ID_VARIABLEEDITORPANELUPBUTTON, - ID_VARIABLEEDITORPANELDOWNBUTTON, ID_VARIABLEEDITORPANELSTATICTEXT1, - ID_VARIABLEEDITORPANELSTATICTEXT2, ID_VARIABLEEDITORPANELSTATICTEXT3, -] = [wx.NewId() for _init_ctrls in range(12)] + ID_VARIABLEEDITORPANELDOWNBUTTON, +] = [wx.NewId() for _init_ctrls in range(13)] class VariablePanel(wx.Panel): @@ -331,9 +335,11 @@ parent.AddGrowableRow(1) def _init_coll_ControlPanelSizer_Items(self, parent): - parent.AddWindow(self.staticText1, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) + parent.AddWindow(self.ReturnTypeLabel, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) parent.AddWindow(self.ReturnType, 0, border=0, flag=0) - parent.AddWindow(self.staticText2, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) + parent.AddWindow(self.DescriptionLabel, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) + parent.AddWindow(self.Description, 0, border=0, flag=0) + parent.AddWindow(self.ClassFilterLabel, 0, border=0, flag=wx.ALIGN_CENTER_VERTICAL) parent.AddWindow(self.ClassFilter, 0, border=0, flag=0) parent.AddWindow(self.AddButton, 0, border=0, flag=0) parent.AddWindow(self.DeleteButton, 0, border=0, flag=0) @@ -346,7 +352,7 @@ def _init_sizers(self): self.MainSizer = wx.FlexGridSizer(cols=1, hgap=10, rows=2, vgap=0) - self.ControlPanelSizer = wx.FlexGridSizer(cols=8, hgap=5, rows=1, vgap=5) + self.ControlPanelSizer = wx.FlexGridSizer(cols=10, hgap=5, rows=1, vgap=5) self._init_coll_MainSizer_Items(self.MainSizer) self._init_coll_MainSizer_Growables(self.MainSizer) @@ -379,8 +385,8 @@ size=wx.Size(0, 0), style=wx.TAB_TRAVERSAL) self.ControlPanel.SetScrollRate(10, 0) - self.staticText1 = wx.StaticText(id=ID_VARIABLEEDITORPANELSTATICTEXT1, - label=_('Return Type:'), name='staticText1', parent=self.ControlPanel, + self.ReturnTypeLabel = wx.StaticText(id=ID_VARIABLEEDITORPANELRETURNTYPELABEL, + label=_('Return Type:'), name='ReturnTypeLabel', parent=self.ControlPanel, pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) self.ReturnType = wx.ComboBox(id=ID_VARIABLEEDITORPANELRETURNTYPE, @@ -388,8 +394,18 @@ size=wx.Size(145, 28), style=wx.CB_READONLY) self.Bind(wx.EVT_COMBOBOX, self.OnReturnTypeChanged, id=ID_VARIABLEEDITORPANELRETURNTYPE) - self.staticText2 = wx.StaticText(id=ID_VARIABLEEDITORPANELSTATICTEXT2, - label=_('Class Filter:'), name='staticText2', parent=self.ControlPanel, + self.DescriptionLabel = wx.StaticText(id=ID_VARIABLEEDITORPANELDESCRIPTIONLABEL, + label=_('Description:'), name='DescriptionLabel', parent=self.ControlPanel, + pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) + + self.Description = wx.TextCtrl(id=ID_VARIABLEEDITORPANELDESCRIPTION, + name='Description', parent=self.ControlPanel, pos=wx.Point(0, 0), + size=wx.Size(250, 28), style=wx.TE_PROCESS_ENTER) + self.Bind(wx.EVT_TEXT_ENTER, self.OnDescriptionChanged, id=ID_VARIABLEEDITORPANELDESCRIPTION) + self.Description.Bind(wx.EVT_KILL_FOCUS, self.OnDescriptionChanged) + + self.ClassFilterLabel = wx.StaticText(id=ID_VARIABLEEDITORPANELCLASSFILTERLABEL, + label=_('Class Filter:'), name='ClassFilterLabel', parent=self.ControlPanel, pos=wx.Point(0, 0), size=wx.DefaultSize, style=0) self.ClassFilter = wx.ComboBox(id=ID_VARIABLEEDITORPANELCLASSFILTER, @@ -577,15 +593,15 @@ def RefreshView(self): self.PouNames = self.Controler.GetProjectPouNames(self.Debug) + returnType = None + description = None words = self.TagName.split("::") if self.ElementType == "config": self.PouIsUsed = False - returnType = None self.Values = self.Controler.GetConfigurationGlobalVars(words[1], self.Debug) elif self.ElementType == "resource": self.PouIsUsed = False - returnType = None self.Values = self.Controler.GetConfigurationResourceGlobalVars(words[1], words[2], self.Debug) else: if self.ElementType == "function": @@ -593,21 +609,30 @@ for base_type in self.Controler.GetDataTypes(self.TagName, True, debug=self.Debug): self.ReturnType.Append(base_type) returnType = self.Controler.GetEditedElementInterfaceReturnType(self.TagName) - else: - returnType = None + description = self.Controler.GetPouDescription(words[1]) self.PouIsUsed = self.Controler.PouIsUsed(words[1]) self.Values = self.Controler.GetEditedElementInterfaceVars(self.TagName, self.Debug) if returnType is not None: self.ReturnType.SetStringSelection(returnType) - self.ReturnType.Enable(self.Debug) - self.staticText1.Show() + self.ReturnType.Enable(not self.Debug) + self.ReturnTypeLabel.Show() self.ReturnType.Show() else: self.ReturnType.Enable(False) - self.staticText1.Hide() + self.ReturnTypeLabel.Hide() self.ReturnType.Hide() + if description is not None: + self.Description.SetValue(description) + self.Description.Enable(not self.Debug) + self.DescriptionLabel.Show() + self.Description.Show() + else: + self.Description.Enable(False) + self.DescriptionLabel.Hide() + self.Description.Hide() + self.RefreshValues() self.VariablesGrid.RefreshButtons() @@ -619,6 +644,15 @@ self.ParentWindow._Refresh(TITLE, FILEMENU, EDITMENU, INSTANCESTREE, LIBRARYTREE) event.Skip() + def OnDescriptionChanged(self, event): + words = self.TagName.split("::") + old_description = self.Controler.GetPouDescription(words[1]) + new_description = self.Description.GetValue() + if new_description != old_description: + self.Controler.SetPouDescription(words[1], new_description) + self.ParentWindow._Refresh(TITLE, FILEMENU, EDITMENU, INSTANCESTREE, LIBRARYTREE) + event.Skip() + def OnClassFilter(self, event): self.Filter = self.FilterChoiceTransfer[VARIABLE_CHOICES_DICT[self.ClassFilter.GetStringSelection()]] self.RefreshTypeList() @@ -660,12 +694,12 @@ self.ParentWindow.RefreshView(variablepanel = False) self.ParentWindow._Refresh(TITLE, FILEMENU, EDITMENU, INSTANCESTREE, LIBRARYTREE) event.Skip() - elif colname == "Location" and LOCATION_MODEL.match(value) is None: - message = _("\"%s\" is not a valid location!") % value else: self.SaveValues() if colname == "Class": self.ParentWindow.RefreshView(variablepanel = False) + elif colname == "Location": + wx.CallAfter(self.ParentWindow.RefreshView) if message is not None: dialog = wx.MessageDialog(self, message, _("Error"), wx.OK|wx.ICON_ERROR) diff -r efedc9d06a59 -r b7062a7018ec graphics/FBD_Objects.py --- a/graphics/FBD_Objects.py Thu Jan 12 17:04:22 2012 +0100 +++ b/graphics/FBD_Objects.py Tue Jan 17 16:44:00 2012 +0100 @@ -41,6 +41,7 @@ def __init__(self, parent, type, name, id = None, extension = 0, inputs = None, connectors = {}, executionControl = False, executionOrder = 0): Graphic_Element.__init__(self, parent) self.Type = None + self.Description = None self.Extension = None self.ExecutionControl = False self.Id = id @@ -233,10 +234,13 @@ for i in xrange(self.Extension - len(blocktype["inputs"])): start += 1 inputs.append(("IN%d"%start, inputs[-1][1], inputs[-1][2])) + comment = blocktype["comment"] + self.Description = _(comment) + blocktype.get("usage", "") else: self.Colour = wx.RED inputs = connectors.get("inputs", []) outputs = connectors.get("outputs", []) + self.Description = None if self.ExecutionControl: inputs.insert(0, ("EN","BOOL","none")) outputs.insert(0, ("ENO","BOOL","none")) @@ -368,6 +372,9 @@ for output in self.Outputs: output.RefreshWires() + def GetToolTipValue(self): + return self.Description + # Adds an highlight to the block def AddHighlight(self, infos, start, end ,highlight_type): if infos[0] in ["type", "name"] and start[0] == 0 and end[0] == 0: diff -r efedc9d06a59 -r b7062a7018ec graphics/GraphicCommons.py --- a/graphics/GraphicCommons.py Thu Jan 12 17:04:22 2012 +0100 +++ b/graphics/GraphicCommons.py Tue Jan 17 16:44:00 2012 +0100 @@ -101,6 +101,9 @@ # Define highlight refresh inhibition period in second REFRESH_HIGHLIGHT_PERIOD = 0.1 +# Define tooltip wait for displaying period in second +TOOLTIP_WAIT_PERIOD = 0.5 + HANDLE_CURSORS = { (1, 1) : 2, (3, 3) : 2, @@ -602,6 +605,13 @@ self.Size = wx.Size(0, 0) self.BoundingBox = wx.Rect(0, 0, 0, 0) self.Visible = False + self.ToolTip = None + self.ToolTipPos = None + self.ToolTipTimer = wx.Timer(self.Parent, -1) + self.Parent.Bind(wx.EVT_TIMER, self.OnToolTipTimer, self.ToolTipTimer) + + def __del__(self): + self def GetDefinition(self): return [self.Id], [] @@ -967,6 +977,36 @@ return movex, movey return 0, 0 + def OnToolTipTimer(self, event): + value = self.GetToolTipValue() + if value is not None and self.ToolTipPos is not None: + self.ToolTip = ToolTip(self.Parent, value) + self.ToolTip.MoveToolTip(self.ToolTipPos) + self.ToolTip.Show() + + def GetToolTipValue(self): + return None + + def CreateToolTip(self, pos): + value = self.GetToolTipValue() + if value is not None: + self.ToolTipPos = pos + self.ToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) + + def MoveToolTip(self, pos): + if self.ToolTip is not None: + self.ToolTip.MoveToolTip(pos) + elif self.ToolTipPos is not None: + self.ToolTipPos = pos + self.ToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) + + def ClearToolTip(self): + self.ToolTipTimer.Stop() + self.ToolTipPos = None + if self.ToolTip is not None: + self.ToolTip.Destroy() + self.ToolTip = None + # Override this method for defining the method to call for adding an highlight to this element def AddHighlight(self, infos, start, end, highlight_type): pass @@ -1792,7 +1832,6 @@ self.OverStart = False self.OverEnd = False self.ComputingType = False - self.ToolTip = None self.Font = parent.GetMiniFont() def GetDefinition(self): @@ -1806,24 +1845,13 @@ self.StartConnected = None self.EndConnected = None - def CreateToolTip(self, pos): + def GetToolTipValue(self): if self.Value is not None and self.Value != "undefined" and not isinstance(self.Value, BooleanType): if isinstance(self.Value, StringType) and self.Value.find("#") == -1: - computed_value = "\"%s\""%self.Value + return "\"%s\""%self.Value else: - computed_value = str(self.Value) - self.ToolTip = ToolTip(self.Parent, computed_value) - self.ToolTip.MoveToolTip(pos) - self.ToolTip.Show() - - def MoveToolTip(self, pos): - if self.ToolTip is not None: - self.ToolTip.MoveToolTip(pos) - - def ClearToolTip(self): - if self.ToolTip is not None: - self.ToolTip.Destroy() - self.ToolTip = None + return str(self.Value) + return None # Returns the RedrawRect def GetRedrawRect(self, movex = 0, movey = 0): diff -r efedc9d06a59 -r b7062a7018ec plcopen/plcopen.py --- a/plcopen/plcopen.py Thu Jan 12 17:04:22 2012 +0100 +++ b/plcopen/plcopen.py Tue Jan 17 16:44:00 2012 +0100 @@ -433,8 +433,13 @@ def AddCustomBlockType(self, pou): pou_name = pou.getname() pou_type = pou.getpouType() + pou_description = pou.getdescription() + if pou_description != "": + pou_comment = "%s\n%s" % (pou_name, pou_description) + else: + pou_comment = pou_name block_infos = {"name" : pou_name, "type" : pou_type, "extensible" : False, - "inputs" : [], "outputs" : [], "comment" : "", + "inputs" : [], "outputs" : [], "comment" : pou_comment, "generate" : generate_block, "initialise" : initialise_block} if pou.getinterface(): return_type = pou.interface.getreturnType() @@ -477,10 +482,8 @@ block_infos["outputs"].append((var.getname(), var_type["name"].upper(), "none")) else: block_infos["outputs"].append((var.getname(), var_type["name"], "none")) - if pou.getbodyType() in ["FBD","LD","SFC"]: - for instance in pou.getinstances(): - if isinstance(instance, PLCOpenClasses.get("commonObjects_comment", None)): - block_infos["comment"] = instance.getcontentText() + block_infos["usage"] = "\n (%s) => (%s)" % (", ".join(["%s:%s" % (input[1], input[0]) for input in block_infos["inputs"]]), + ", ".join(["%s:%s" % (output[1], output[0]) for output in block_infos["outputs"]])) self.CustomBlockTypes.append(block_infos) setattr(cls, "AddCustomBlockType", AddCustomBlockType) @@ -1188,6 +1191,21 @@ cls = PLCOpenClasses.get("pous_pou", None) if cls: + def setdescription(self, description): + doc = self.getdocumentation() + if doc is None: + doc = PLCOpenClasses["formattedText"]() + self.setdocumentation(doc) + doc.settext(description) + setattr(cls, "setdescription", setdescription) + + def getdescription(self): + doc = self.getdocumentation() + if doc is not None: + return doc.gettext() + return "" + setattr(cls, "getdescription", getdescription) + def setbodyType(self, type): if len(self.body) > 0: if type == "IL":