editors/ResourceEditor.py
author Andrey Skvortsov <andrej.skvortzov@gmail.com>
Thu, 01 Jun 2017 14:37:14 +0300
changeset 1686 85fdcc04da25
parent 1657 3a98027ff372
child 1734 750eeb7230a1
permissions -rw-r--r--
disallow setting IEC location addresses for TIME, DATE, DT and TOD variables

Size of these types is not defined and depends on the implementation
(IEC 61131-3 2.3.1).
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of Beremiz, a Integrated Development Environment for
# programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
#
# Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD
#
# See COPYING file for copyrights details.
#
# This program 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
# of the License, or (at your option) any later version.
#
# This program 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 program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import wx
import wx.lib.buttons
import wx.grid

from graphics.GraphicCommons import REFRESH_HIGHLIGHT_PERIOD, ERROR_HIGHLIGHT
from controls import CustomGrid, CustomTable, DurationCellEditor
from dialogs.DurationEditorDialog import IEC_TIME_MODEL
from EditorPanel import EditorPanel
from util.BitmapLibrary import GetBitmap
from plcopen.structures import LOCATIONDATATYPES, TestIdentifier, IEC_KEYWORDS, DefaultType
#-------------------------------------------------------------------------------
#                          Configuration Editor class
#-------------------------------------------------------------------------------

[ID_CONFIGURATIONEDITOR,
] = [wx.NewId() for _init_ctrls in range(1)]

class ConfigurationEditor(EditorPanel):

    ID = ID_CONFIGURATIONEDITOR
    VARIABLE_PANEL_TYPE = "config"

    def GetBufferState(self):
        return self.Controler.GetBufferState()

    def Undo(self):
        self.Controler.LoadPrevious()
        self.ParentWindow.CloseTabsWithoutModel()

    def Redo(self):
        self.Controler.LoadNext()
        self.ParentWindow.CloseTabsWithoutModel()

    def HasNoModel(self):
        return self.Controler.GetEditedElement(self.TagName) is None


#-------------------------------------------------------------------------------
#                            Resource Editor class
#-------------------------------------------------------------------------------

def GetTasksTableColnames():
    _ = lambda x : x
    return [_("Name"), _("Triggering"), _("Single"), _("Interval"), _("Priority")]

def GetTaskTriggeringOptions():
    _ = lambda x : x
    return [_("Interrupt"), _("Cyclic")]
TASKTRIGGERINGOPTIONS_DICT = dict([(_(option), option) for option in GetTaskTriggeringOptions()])

SingleCellEditor = lambda *x : wx.grid.GridCellChoiceEditor()

def CheckSingle(single, varlist):
    return single in varlist


def GetInstancesTableColnames():
    _ = lambda x : x
    return [_("Name"), _("Type"), _("Task")]

class ResourceTable(CustomTable):

    """
    A custom wx.grid.Grid Table using user supplied data
    """
    def __init__(self, parent, data, colnames):
        # The base class must be initialized *first*
        CustomTable.__init__(self, parent, data, colnames)
        self.ColAlignements = []
        self.ColSizes = []

    def GetColAlignements(self):
        return self.ColAlignements

    def SetColAlignements(self, list):
        self.ColAlignements = list

    def GetColSizes(self):
        return self.ColSizes

    def SetColSizes(self, list):
        self.ColSizes = list

    def GetValue(self, row, col):
        if row < self.GetNumberRows():
            colname = self.GetColLabelValue(col, False)
            value = self.data[row].get(colname, "")
            if colname == "Triggering":
                return _(value)
            return value

    def SetValue(self, row, col, value):
        if col < len(self.colnames):
            colname = self.GetColLabelValue(col, False)
            if colname == "Triggering":
                value = TASKTRIGGERINGOPTIONS_DICT[value]
            self.data[row][colname] = value

    def _updateColAttrs(self, grid):
        """
        wx.grid.Grid -> update the column attributes to add the
        appropriate renderer given the column name.

        Otherwise default to the default renderer.
        """

        for col in range(self.GetNumberCols()):
            attr = wx.grid.GridCellAttr()
            attr.SetAlignment(self.ColAlignements[col], wx.ALIGN_CENTRE)
            grid.SetColAttr(col, attr)
            grid.SetColSize(col, self.ColSizes[col])

        for row in range(self.GetNumberRows()):
            row_highlights = self.Highlights.get(row, {})
            for col in range(self.GetNumberCols()):
                editor = None
                renderer = None
                error = False
                colname = self.GetColLabelValue(col, False)
                grid.SetReadOnly(row, col, False)
                if colname == "Name":
                    editor = wx.grid.GridCellTextEditor()
                    renderer = wx.grid.GridCellStringRenderer()
                elif colname == "Interval":
                    editor = DurationCellEditor(self, colname)
                    renderer = wx.grid.GridCellStringRenderer()
                    if self.GetValueByName(row, "Triggering") != "Cyclic":
                        grid.SetReadOnly(row, col, True)
                    interval = self.GetValueByName(row, colname)
                    if interval != "" and IEC_TIME_MODEL.match(interval.upper()) is None:
                        error = True
                elif colname == "Single":
                    editor = SingleCellEditor(self,colname)
                    editor.SetParameters(self.Parent.VariableList)
                    if self.GetValueByName(row, "Triggering") != "Interrupt":
                        grid.SetReadOnly(row, col, True)
                    single = self.GetValueByName(row, colname)
                    if single != "" and not CheckSingle(single,self.Parent.VariableList):
                        error = True
                elif colname == "Triggering":
                    editor = wx.grid.GridCellChoiceEditor()
                    editor.SetParameters(",".join(map(_, GetTaskTriggeringOptions())))
                elif colname == "Type":
                    editor = wx.grid.GridCellChoiceEditor()
                    editor.SetParameters(self.Parent.TypeList)
                elif colname == "Priority":
                    editor = wx.grid.GridCellNumberEditor()
                    editor.SetParameters("0,65535")
                elif colname == "Task":
                    editor = wx.grid.GridCellChoiceEditor()
                    editor.SetParameters(self.Parent.TaskList)

                grid.SetCellEditor(row, col, editor)
                grid.SetCellRenderer(row, col, renderer)

                if error:
                    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)


#-------------------------------------------------------------------------------
#                        Highlights showing functions
#-------------------------------------------------------------------------------

    def AddHighlight(self, infos, highlight_type):
        row_highlights = self.Highlights.setdefault(infos[0], {})
        col_highlights = row_highlights.setdefault(infos[1], [])
        col_highlights.append(highlight_type)

    def ClearHighlights(self, highlight_type=None):
        if highlight_type is None:
            self.Highlights = {}
        else:
            for row, row_highlights in self.Highlights.iteritems():
                row_items = row_highlights.items()
                for col, col_highlights in row_items:
                    if highlight_type in col_highlights:
                        col_highlights.remove(highlight_type)
                    if len(col_highlights) == 0:
                        row_highlights.pop(col)



class ResourceEditor(EditorPanel):

    VARIABLE_PANEL_TYPE = "resource"

    def _init_Editor(self, parent):
        self.Editor = wx.Panel(parent, style=wx.SUNKEN_BORDER|wx.TAB_TRAVERSAL)

        main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=5)
        main_sizer.AddGrowableCol(0)
        main_sizer.AddGrowableRow(0)
        main_sizer.AddGrowableRow(1)

        tasks_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=5)
        tasks_sizer.AddGrowableCol(0)
        tasks_sizer.AddGrowableRow(1)
        main_sizer.AddSizer(tasks_sizer, border=5,
              flag=wx.GROW|wx.TOP|wx.LEFT|wx.RIGHT)

        tasks_buttons_sizer = wx.FlexGridSizer(cols=5, hgap=5, rows=1, vgap=0)
        tasks_buttons_sizer.AddGrowableCol(0)
        tasks_buttons_sizer.AddGrowableRow(0)
        tasks_sizer.AddSizer(tasks_buttons_sizer, flag=wx.GROW)

        tasks_label = wx.StaticText(self.Editor, label=_(u'Tasks:'))
        tasks_buttons_sizer.AddWindow(tasks_label, flag=wx.ALIGN_BOTTOM)

        for name, bitmap, help in [
                ("AddTaskButton", "add_element", _("Add task")),
                ("DeleteTaskButton", "remove_element", _("Remove task")),
                ("UpTaskButton", "up", _("Move task up")),
                ("DownTaskButton", "down", _("Move task down"))]:
            button = wx.lib.buttons.GenBitmapButton(self.Editor,
                  bitmap=GetBitmap(bitmap), size=wx.Size(28, 28), style=wx.NO_BORDER)
            button.SetToolTipString(help)
            setattr(self, name, button)
            tasks_buttons_sizer.AddWindow(button)

        self.TasksGrid = CustomGrid(self.Editor, style=wx.VSCROLL)
        self.TasksGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnTasksGridCellChange)
        tasks_sizer.AddWindow(self.TasksGrid, flag=wx.GROW)

        instances_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=5)
        instances_sizer.AddGrowableCol(0)
        instances_sizer.AddGrowableRow(1)
        main_sizer.AddSizer(instances_sizer, border=5,
              flag=wx.GROW|wx.BOTTOM|wx.LEFT|wx.RIGHT)

        instances_buttons_sizer = wx.FlexGridSizer(cols=5, hgap=5, rows=1, vgap=0)
        instances_buttons_sizer.AddGrowableCol(0)
        instances_buttons_sizer.AddGrowableRow(0)
        instances_sizer.AddSizer(instances_buttons_sizer, flag=wx.GROW)

        instances_label = wx.StaticText(self.Editor, label=_(u'Instances:'))
        instances_buttons_sizer.AddWindow(instances_label, flag=wx.ALIGN_BOTTOM)

        for name, bitmap, help in [
                ("AddInstanceButton", "add_element", _("Add instance")),
                ("DeleteInstanceButton", "remove_element", _("Remove instance")),
                ("UpInstanceButton", "up", _("Move instance up")),
                ("DownInstanceButton", "down", _("Move instance down"))]:
            button = wx.lib.buttons.GenBitmapButton(self.Editor,
                  bitmap=GetBitmap(bitmap), size=wx.Size(28, 28), style=wx.NO_BORDER)
            button.SetToolTipString(help)
            setattr(self, name, button)
            instances_buttons_sizer.AddWindow(button)

        self.InstancesGrid = CustomGrid(self.Editor, style=wx.VSCROLL)
        self.InstancesGrid.Bind(wx.grid.EVT_GRID_CELL_CHANGE,
              self.OnInstancesGridCellChange)
        instances_sizer.AddWindow(self.InstancesGrid, flag=wx.GROW)

        self.Editor.SetSizer(main_sizer)

    def __init__(self, parent, tagname, window, controler):
        EditorPanel.__init__(self, parent, tagname, window, controler)

        self.RefreshHighlightsTimer = wx.Timer(self, -1)
        self.Bind(wx.EVT_TIMER, self.OnRefreshHighlightsTimer, self.RefreshHighlightsTimer)

        self.TasksDefaultValue = {"Name" : "", "Triggering" : "", "Single" : "", "Interval" : "", "Priority" : 0}
        self.TasksTable = ResourceTable(self, [], GetTasksTableColnames())
        self.TasksTable.SetColAlignements([wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_RIGHT, wx.ALIGN_RIGHT])
        self.TasksTable.SetColSizes([200, 100, 100, 150, 100])
        self.TasksGrid.SetTable(self.TasksTable)
        self.TasksGrid.SetButtons({"Add": self.AddTaskButton,
                                   "Delete": self.DeleteTaskButton,
                                   "Up": self.UpTaskButton,
                                   "Down": self.DownTaskButton})

        def _AddTask(new_row):
            self.TasksTable.InsertRow(new_row, self.TasksDefaultValue.copy())
            self.RefreshModel()
            self.RefreshView()
            return new_row
        setattr(self.TasksGrid, "_AddRow", _AddTask)

        def _DeleteTask(row):
            self.TasksTable.RemoveRow(row)
            self.RefreshModel()
            self.RefreshView()
        setattr(self.TasksGrid, "_DeleteRow", _DeleteTask)

        def _MoveTask(row, move):
            new_row = self.TasksTable.MoveRow(row, move)
            if new_row != row:
                self.RefreshModel()
                self.RefreshView()
            return new_row
        setattr(self.TasksGrid, "_MoveRow", _MoveTask)

        self.TasksGrid.SetRowLabelSize(0)
        self.TasksTable.ResetView(self.TasksGrid)
        self.TasksGrid.RefreshButtons()

        self.InstancesDefaultValue = {"Name" : "", "Type" : "", "Task" : ""}
        self.InstancesTable = ResourceTable(self, [], GetInstancesTableColnames())
        self.InstancesTable.SetColAlignements([wx.ALIGN_LEFT, wx.ALIGN_LEFT, wx.ALIGN_LEFT])
        self.InstancesTable.SetColSizes([200, 150, 150])
        self.InstancesGrid.SetTable(self.InstancesTable)
        self.InstancesGrid.SetButtons({"Add": self.AddInstanceButton,
                                       "Delete": self.DeleteInstanceButton,
                                       "Up": self.UpInstanceButton,
                                       "Down": self.DownInstanceButton})

        def _AddInstance(new_row):
            self.InstancesTable.InsertRow(new_row, self.InstancesDefaultValue.copy())
            self.RefreshModel()
            self.RefreshView()
            return new_row
        setattr(self.InstancesGrid, "_AddRow", _AddInstance)

        def _DeleteInstance(row):
            self.InstancesTable.RemoveRow(row)
            self.RefreshModel()
            self.RefreshView()
        setattr(self.InstancesGrid, "_DeleteRow", _DeleteInstance)

        def _MoveInstance(row, move):
            new_row = max(0, min(row + move, self.InstancesTable.GetNumberRows() - 1))
            if new_row != row:
                if self.InstancesTable.GetValueByName(row, "Task") != self.InstancesTable.GetValueByName(new_row, "Task"):
                    return row
                self.InstancesTable.MoveRow(row, move)
                self.RefreshModel()
                self.RefreshView()
            return new_row
        setattr(self.InstancesGrid, "_MoveRow", _MoveInstance)

        def _RefreshInstanceButtons():
            if self:
                rows = self.InstancesTable.GetNumberRows()
                row = self.InstancesGrid.GetGridCursorRow()
                self.DeleteInstanceButton.Enable(rows > 0)
                self.UpInstanceButton.Enable(row > 0 and
                    self.InstancesTable.GetValueByName(row, "Task") == self.InstancesTable.GetValueByName(row - 1, "Task"))
                self.DownInstanceButton.Enable(0 <= row < rows - 1 and
                    self.InstancesTable.GetValueByName(row, "Task") == self.InstancesTable.GetValueByName(row + 1, "Task"))
        setattr(self.InstancesGrid, "RefreshButtons", _RefreshInstanceButtons)

        self.InstancesGrid.SetRowLabelSize(0)
        self.InstancesTable.ResetView(self.InstancesGrid)
        self.InstancesGrid.RefreshButtons()

        self.TasksGrid.SetFocus()

    def __del__(self):
        self.RefreshHighlightsTimer.Stop()

    def RefreshTypeList(self):
        self.TypeList = ""
        blocktypes = self.Controler.GetBlockResource()
        for blocktype in blocktypes:
            self.TypeList += ",%s"%blocktype

    def RefreshTaskList(self):
        self.TaskList = ""
        for row in xrange(self.TasksTable.GetNumberRows()):
            self.TaskList += ",%s"%self.TasksTable.GetValueByName(row, "Name")

    def RefreshVariableList(self):
        self.VariableList = ""
        for variable in self.Controler.GetEditedResourceVariables(self.TagName):
            self.VariableList += ",%s"%variable

    def RefreshModel(self):
        self.Controler.SetEditedResourceInfos(self.TagName, self.TasksTable.GetData(), self.InstancesTable.GetData())
        self.RefreshBuffer()

    # Buffer the last model state
    def RefreshBuffer(self):
        self.Controler.BufferProject()
        self.ParentWindow.RefreshTitle()
        self.ParentWindow.RefreshFileMenu()
        self.ParentWindow.RefreshEditMenu()

    def GetBufferState(self):
        return self.Controler.GetBufferState()

    def Undo(self):
        self.Controler.LoadPrevious()
        self.ParentWindow.CloseTabsWithoutModel()

    def Redo(self):
        self.Controler.LoadNext()
        self.ParentWindow.CloseTabsWithoutModel()

    def HasNoModel(self):
        return self.Controler.GetEditedElement(self.TagName) is None

    def RefreshView(self, variablepanel=True):
        EditorPanel.RefreshView(self, variablepanel)

        tasks, instances = self.Controler.GetEditedResourceInfos(self.TagName)
        self.TasksTable.SetData(tasks)
        self.InstancesTable.SetData(instances)
        self.RefreshTypeList()
        self.RefreshTaskList()
        self.RefreshVariableList()
        self.TasksTable.ResetView(self.TasksGrid)
        self.InstancesTable.ResetView(self.InstancesGrid)
        self.TasksGrid.RefreshButtons()
        self.InstancesGrid.RefreshButtons()

    def ShowErrorMessage(self, message):
        dialog = wx.MessageDialog(self, message, _("Error"), wx.OK|wx.ICON_ERROR)
        dialog.ShowModal()
        dialog.Destroy()

    def OnTasksGridCellChange(self, event):
        row, col = event.GetRow(), event.GetCol()
        if self.TasksTable.GetColLabelValue(col, False) == "Name":
            value = self.TasksTable.GetValue(row, col)
            message = None

            if not TestIdentifier(value):
                message = _("\"%s\" is not a valid identifier!") % value
            elif value.upper() in IEC_KEYWORDS:
                message = _("\"%s\" is a keyword. It can't be used!") % value
            elif value.upper() in [var["Name"].upper() for i, var in enumerate(self.TasksTable.data) if i!=row]:
                message = _("A task with the same name already exists!")
            if message is not None:
                event.Veto()
                wx.CallAfter(self.ShowErrorMessage, message)
                return

            tasklist = [name for name in self.TaskList.split(",") if name != ""]
            for i in xrange(self.TasksTable.GetNumberRows()):
                task = self.TasksTable.GetValueByName(i, "Name")
                if task in tasklist:
                    tasklist.remove(task)
            if len(tasklist) > 0:
                old_name = tasklist[0].upper()
                new_name = self.TasksTable.GetValue(row, col)
                for i in xrange(self.InstancesTable.GetNumberRows()):
                    name = self.InstancesTable.GetValueByName(i, "Task").upper()
                    if old_name == name:
                        self.InstancesTable.SetValueByName(i, "Task", new_name)
        self.RefreshModel()
        colname = self.TasksTable.GetColLabelValue(col, False)
        if colname in ["Triggering", "Name", "Single", "Interval"]:
            wx.CallAfter(self.RefreshView, False)
        event.Skip()

    def OnInstancesGridCellChange(self, event):
        row, col = event.GetRow(), event.GetCol()
        if self.InstancesTable.GetColLabelValue(col, False) == "Name":
            value = self.InstancesTable.GetValue(row, col)
            message = None

            if not TestIdentifier(value):
                message = _("\"%s\" is not a valid identifier!") % value
            elif value.upper() in IEC_KEYWORDS:
                message = _("\"%s\" is a keyword. It can't be used!") % value
            elif value.upper() in [var["Name"].upper() for i ,var in enumerate(self.InstancesTable.data) if i!=row]:
                message = _("An instance with the same name already exists!")
            if message is not None:
                event.Veto()
                wx.CallAfter(self.ShowErrorMessage, message)
                return

        self.RefreshModel()
        self.ParentWindow.RefreshPouInstanceVariablesPanel()
        self.InstancesGrid.RefreshButtons()
        event.Skip()

#-------------------------------------------------------------------------------
#                        Highlights showing functions
#-------------------------------------------------------------------------------

    def OnRefreshHighlightsTimer(self, event):
        self.RefreshView()
        event.Skip()

    def AddHighlight(self, infos, start, end, highlight_type):
        EditorPanel.AddHighlight(self, infos, start, end, highlight_type)

        if infos[0] == "task":
            self.TasksTable.AddHighlight(infos[1:], highlight_type)
        elif infos[0] == "instance":
            self.InstancesTable.AddHighlight(infos[1:], highlight_type)
        self.RefreshHighlightsTimer.Start(int(REFRESH_HIGHLIGHT_PERIOD * 1000), oneShot=True)

    def ClearHighlights(self, highlight_type=None):
        EditorPanel.ClearHighlights(self, highlight_type)

        self.TasksTable.ClearHighlights(highlight_type)
        self.InstancesTable.ClearHighlights(highlight_type)
        self.TasksTable.ResetView(self.TasksGrid)
        self.InstancesTable.ResetView(self.InstancesGrid)