introduced BrowseVariableLocationsDialog and the GridCellEditor that launches it.
authorb.taylor@willowglen.ca
Fri, 04 Sep 2009 16:37:52 -0600
changeset 422 31c3dc45cfab
parent 421 9855343da6fc
child 423 53aa0c334f2f
introduced BrowseVariableLocationsDialog and the GridCellEditor that launches it.
VariablePanel.py
--- a/VariablePanel.py	Thu Sep 03 09:37:23 2009 -0600
+++ b/VariablePanel.py	Fri Sep 04 16:37:52 2009 -0600
@@ -177,7 +177,7 @@
                         renderer = wx.grid.GridCellStringRenderer()
                     elif colname == "Location":
                         if self.GetValueByName(row, "Class") in ["Local", "Global"]:
-                            editor = wx.grid.GridCellTextEditor()
+                            editor = LocationCellEditor(self.Parent)
                             renderer = wx.grid.GridCellStringRenderer()
                         else:
                             grid.SetReadOnly(row, col, True)
@@ -638,6 +638,7 @@
         row, col = event.GetRow(), event.GetCol()
         colname = self.Table.GetColLabelValue(col)
         value = self.Table.GetValue(row, col)
+
         if colname == "Name" and value != "":
             if not TestIdentifier(value):
                 message = wx.MessageDialog(self, _("\"%s\" is not a valid identifier!")%value, _("Error"), wx.OK|wx.ICON_ERROR)
@@ -679,32 +680,53 @@
     
     def OnVariablesGridEditorShown(self, event):
         row, col = event.GetRow(), event.GetCol() 
-        classtype = self.Table.GetValueByName(row, "Class")
-        if self.Table.GetColLabelValue(col) == "Type":
-            type_menu = wx.Menu(title='')
+
+        label_value = self.Table.GetColLabelValue(col)
+        if label_value == "Type":
+            debug = self.ParentWindow.Debug
+            type_menu = wx.Menu(title='')   # the root menu
+
+            # build a submenu containing standard IEC types
             base_menu = wx.Menu(title='')
             for base_type in self.Controler.GetBaseTypes():
                 new_id = wx.NewId()
                 AppendMenu(base_menu, help='', id=new_id, kind=wx.ITEM_NORMAL, text=base_type)
                 self.Bind(wx.EVT_MENU, self.GetVariableTypeFunction(base_type), id=new_id)
+
             type_menu.AppendMenu(wx.NewId(), _("Base Types"), base_menu)
+
+            # build a submenu containing user-defined types
             datatype_menu = wx.Menu(title='')
-            for datatype in self.Controler.GetDataTypes(basetypes = False, debug = self.ParentWindow.Debug):
+            datatypes = self.Controler.GetDataTypes(basetypes = False, debug = debug)
+            for datatype in datatypes:
                 new_id = wx.NewId()
                 AppendMenu(datatype_menu, help='', id=new_id, kind=wx.ITEM_NORMAL, text=datatype)
                 self.Bind(wx.EVT_MENU, self.GetVariableTypeFunction(datatype), id=new_id)
+
             type_menu.AppendMenu(wx.NewId(), _("User Data Types"), datatype_menu)
-            functionblock_menu = wx.Menu(title='')
-            bodytype = self.Controler.GetEditedElementBodyType(self.TagName, self.ParentWindow.Debug)
-            pouname, poutype = self.Controler.GetEditedElementType(self.TagName, self.ParentWindow.Debug)
-            if classtype in ["Input","Output","InOut","External","Global"] or poutype != "function" and bodytype in ["ST", "IL"]:
-                for functionblock_type in self.Controler.GetFunctionBlockTypes(self.TagName, self.ParentWindow.Debug):
+
+            # build a submenu containing function block types
+            bodytype = self.Controler.GetEditedElementBodyType(self.TagName, debug)
+            pouname, poutype = self.Controler.GetEditedElementType(self.TagName, debug)
+            classtype = self.Table.GetValueByName(row, "Class")
+
+            if classtype in ["Input", "Output", "InOut", "External", "Global"] or \
+            poutype != "function" and bodytype in ["ST", "IL"]:
+                functionblock_menu = wx.Menu(title='')
+                fbtypes = self.Controler.GetFunctionBlockTypes(self.TagName, debug)
+                for functionblock_type in fbtypes:
                     new_id = wx.NewId()
                     AppendMenu(functionblock_menu, help='', id=new_id, kind=wx.ITEM_NORMAL, text=functionblock_type)
                     self.Bind(wx.EVT_MENU, self.GetVariableTypeFunction(functionblock_type), id=new_id)
+
                 type_menu.AppendMenu(wx.NewId(), _("Function Block Types"), functionblock_menu)
+
             rect = self.VariablesGrid.BlockToDeviceRect((row, col), (row, col))
-            self.VariablesGrid.PopupMenuXY(type_menu, rect.x + rect.width, rect.y + self.VariablesGrid.GetColLabelSize())
+            corner_x = rect.x + rect.width
+            corner_y = rect.y + self.VariablesGrid.GetColLabelSize()
+
+            # pop up this new menu
+            self.VariablesGrid.PopupMenuXY(type_menu, corner_x, corner_y)
             event.Veto()
         else:
             event.Skip()
@@ -803,3 +825,317 @@
     def ClearErrors(self):
         self.Table.ClearErrors()
         self.Table.ResetView(self.VariablesGrid)
+
+class LocationCellControl(wx.PyControl):
+    '''
+    Custom cell editor control with a text box and a button that launches
+    the BrowseVariableLocationsDialog.
+    '''
+    def __init__(self, parent, var_panel):
+        wx.Control.__init__(self, parent, -1)
+        self.ParentWindow = parent
+        self.VarPanel = var_panel
+        self.Row = -1
+
+        self.Bind(wx.EVT_SIZE, self.OnSize)
+
+        # create text control
+        self.txt = wx.TextCtrl(self, -1, '')
+
+        # create browse button
+        self.btn = wx.Button(self, -1, '...', size=(20,20))
+        self.btn.Bind(wx.EVT_BUTTON, self.OnBtnBrowseClick)
+
+        szr = wx.BoxSizer(wx.HORIZONTAL)
+        szr.Add(self.txt, proportion=1, flag=wx.EXPAND)
+        szr.Add(self.btn, proportion=0, flag=wx.ALIGN_RIGHT)
+
+        szr.SetSizeHints(self)
+        self.SetSizer(szr)
+        self.Layout()
+
+    def SetRow(self, row):
+        '''set the grid row that we're working on'''
+        self.Row = row
+
+    def OnSize(self, event):
+        '''resize the button and text control to fit'''
+        overall_width = self.GetSize()[0]
+        btn_width, btn_height = self.btn.GetSize()
+        new_txt_width = overall_width - btn_width
+
+        self.txt.SetSize(wx.Size(new_txt_width, -1))
+        self.btn.SetDimensions(new_txt_width, -1, btn_width, btn_height)
+
+    def OnBtnBrowseClick(self, event):
+        # pop up the location browser dialog
+        dia = BrowseVariableLocationsDialog(self.ParentWindow, self.VarPanel)
+        dia.ShowModal()
+
+        if dia.Selection:
+            loc, iec_type = dia.Selection
+
+            # set the location
+            self.SetText(loc)
+
+            # set the variable type
+            # NOTE: this update won't be displayed until editing is complete
+            # (when EndEdit is called).
+            # we can't call VarPanel.RefreshValues() here because it causes
+            # an exception. 
+            self.VarPanel.Table.SetValueByName(self.Row, 'Type', iec_type)
+
+        self.txt.SetFocus()
+
+    def SetText(self, text):
+        self.txt.SetValue(text)
+
+    def SetInsertionPoint(self, i):
+        self.txt.SetInsertionPoint(i)
+
+    def GetText(self):
+        return self.txt.GetValue()
+
+    def SetFocus(self):
+        self.txt.SetFocus()
+
+class LocationCellEditor(wx.grid.PyGridCellEditor):
+    '''
+    Grid cell editor that uses LocationCellControl to display a browse button.
+    '''
+    def __init__(self, var_panel):
+        wx.grid.PyGridCellEditor.__init__(self)
+        self.VarPanel = var_panel
+
+    def Create(self, parent, id, evt_handler):
+        self.text_browse = LocationCellControl(parent, self.VarPanel)
+        self.SetControl(self.text_browse)
+        if evt_handler:
+            self.text_browse.PushEventHandler(evt_handler)
+
+    def BeginEdit(self, row, col, grid):
+        loc = self.VarPanel.Table.GetValueByName(row, 'Location')
+        self.text_browse.SetText(loc)
+        self.text_browse.SetRow(row)
+        self.text_browse.SetFocus()
+
+    def EndEdit(self, row, col, grid):
+        loc = self.text_browse.GetText()
+        old_loc = self.VarPanel.Table.GetValueByName(row, 'Location')
+
+        if loc != old_loc:
+            self.VarPanel.Table.SetValueByName(row, 'Location', loc)
+            
+            # NOTE: this is a really lame hack to force this row's
+            # 'Type' cell to redraw (since it may have changed). 
+            # There's got to be a better way than this.
+            self.VarPanel.VariablesGrid.AutoSizeRow(row)
+            return True
+
+    def SetSize(self, rect):
+        # -2 and +5 give this some extra vertical padding
+        self.text_browse.SetDimensions(rect.x, rect.y-2,
+                                        rect.width, rect.height+5,
+                                        wx.SIZE_ALLOW_MINUS_ONE)
+
+    def Clone(self):
+        return LocationCellEditor(self.VarPanel)
+
+class BrowseVariableLocationsDialog(wx.Dialog):
+    # turn LOCATIONDATATYPES inside-out
+    LOCATION_SIZES = {}
+    for size, types in LOCATIONDATATYPES.iteritems():
+        for type in types:
+            LOCATION_SIZES[type] = size
+
+    class PluginData:
+        '''contains a plugin's VariableLocationTree'''
+        def __init__(self, plugin):
+            self.subtree  = plugin.GetVariableLocationTree()
+
+    class SubtreeData:
+        '''contains a subtree of a plugin's VariableLocationTree'''
+        def __init__(self, subtree):
+            self.subtree = subtree
+
+    class VariableData:
+        '''contains all the information about a valid variable location'''
+        def __init__(self, type, dir, loc):
+            self.type   = type
+
+            loc_suffix = '.'.join([str(x) for x in loc])
+
+            size = BrowseVariableLocationsDialog.LOCATION_SIZES[type]
+            self.loc = size + loc_suffix
+
+            # if a direction was given, use it
+            # (if not we'll prompt the user to select one)
+            if dir:
+                self.loc = '%' + dir + self.loc
+
+    def __init__(self, parent, var_panel):
+        self.VarPanel   = var_panel
+        self.Selection  = None
+
+        # create the dialog
+        wx.Dialog.__init__(self, parent=parent, title=_('Browse Variables'),
+                            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER,
+                            size=(-1, 400))
+
+        # create the root sizer
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        self.SetSizer(sizer)
+
+        # create the tree control
+        self.tree = wx.TreeCtrl(self, style=wx.TR_DEFAULT_STYLE|wx.TR_HIDE_ROOT)
+        sizer.Add(self.tree, 1, wx.EXPAND|wx.ALL, border=20)
+
+        # create the Direction sizer and field
+        dsizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(dsizer, 0, wx.LEFT|wx.RIGHT|wx.BOTTOM|wx.EXPAND, border=20)
+
+        #   direction label
+        ltext = wx.StaticText(self, -1, _('Direction:'))
+        dsizer.Add(ltext, flag=wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, border=20)
+
+        #   direction choice
+        self.DirChoice = wx.Choice(id=-1, parent=self, choices=[_('Input'), _('Output'), _('Memory')])
+        dsizer.Add(self.DirChoice, flag=wx.EXPAND)
+        #   set a default for the choice   
+        self.SetSelectedDirection('I')
+
+        # create the button sizer
+        btsizer = wx.BoxSizer(wx.HORIZONTAL)
+        sizer.Add(btsizer, 0, wx.LEFT|wx.RIGHT|wx.BOTTOM|wx.ALIGN_RIGHT, border=20)
+
+        # add plugins to the tree
+        root = self.tree.AddRoot(_('Plugins'))
+        ctrl = self.VarPanel.Controler
+        self.AddChildPluginsToTree(ctrl.PluginsRoot, root)
+
+        #       -- buttons --
+
+        # ok button
+        self.OkButton = wx.Button(self, wx.ID_OK, _('Use Location'))
+        self.OkButton.SetDefault()
+        btsizer.Add(self.OkButton, flag=wx.RIGHT, border=5)
+
+        # cancel button
+        b = wx.Button(self, wx.ID_CANCEL, _('Cancel'))
+        btsizer.Add(b)
+
+        #       -- event handlers --
+
+        # accept the location on doubleclick or clicking the Use Location button
+        self.Bind(wx.EVT_BUTTON, self.OnOk, self.OkButton)
+        self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnOk, self.tree)
+
+        # disable the Add button when we're not on a valid variable
+        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelChange, self.tree)
+
+        # handle the Expand event. this lets us create the tree as it's expanded
+        # (trying to create it all at once is slow since plugin variable location
+        # trees can be big)
+        wx.EVT_TREE_ITEM_EXPANDING(self.tree, self.tree.GetId(), self.OnExpand)
+
+    def OnExpand(self, event):
+        item = event.GetItem()
+        if not item.IsOk():
+            item = self.tree.GetSelection()
+        
+        data = self.tree.GetPyData(item)
+        self.ExpandTree(item, data.subtree)
+
+    def AddChildPluginsToTree(self, plugin, root):
+        for p in plugin.IECSortedChilds():
+            plug_name = p.BaseParams.getName()
+            new_item = self.tree.AppendItem(root, plug_name)
+
+            # make it look like the tree item has children (since it doesn't yet)
+            self.tree.SetItemHasChildren(new_item) 
+
+            # attach the plugin data to the tree item
+            self.tree.SetPyData(new_item, BrowseVariableLocationsDialog.PluginData(p))
+
+            # add child plugins recursively
+            self.AddChildPluginsToTree(p, new_item)
+
+    def ExpandTree(self, root, subtree):
+            items = subtree.items()
+            items.sort()
+
+            for node_name, data in items:
+                if isinstance(data, dict):
+                    # this is a new subtree
+
+                    new_item = self.tree.AppendItem(root, node_name)
+                    self.tree.SetItemHasChildren(new_item)
+
+                    # attach the new subtree's data to the tree item
+                    new_data = BrowseVariableLocationsDialog.SubtreeData(data)
+                    self.tree.SetPyData(new_item, new_data)
+                else:
+                    # this is a new leaf.
+
+                    # data is a tuple containing (IEC type, I/Q/M/None, IEC path tuple)
+                    type, dir, loc = data
+
+                    node_name = '%s (%s)' % (node_name, type)
+                    new_item = self.tree.AppendItem(root, node_name)
+
+                    vd = BrowseVariableLocationsDialog.VariableData(type, dir, loc)
+                    self.tree.SetPyData(new_item, vd)
+
+    def OnSelChange(self, event):
+        '''updates the text field and the "Use Location"  button.'''
+        item = self.tree.GetSelection()
+        data = self.tree.GetPyData(item)
+
+        if isinstance(data, BrowseVariableLocationsDialog.VariableData):
+            self.OkButton.Enable()
+
+            location = data.loc
+            if location[0] == '%':
+                # location has a fixed direction
+                self.SetSelectedDirection(location[1])
+                self.DirChoice.Disable()
+            else:
+                # this location can have any direction (user selects)
+                self.DirChoice.Enable()
+        else:
+            self.OkButton.Disable()
+            self.DirChoice.Disable()
+
+    def GetSelectedDirection(self):
+        selected = self.DirChoice.GetSelection()
+        if selected == 0:
+            return 'I'
+        elif selected == 1:
+            return 'Q'
+        else:
+            return 'M'
+
+    def SetSelectedDirection(self, dir_letter):
+        if dir_letter == 'I':
+            self.DirChoice.SetSelection(0)
+        elif dir_letter == 'Q':
+            self.DirChoice.SetSelection(1)
+        else:
+            self.DirChoice.SetSelection(2)
+
+    def OnOk(self, event):
+        item = self.tree.GetSelection()
+        data = self.tree.GetPyData(item)
+
+        if not isinstance(data, BrowseVariableLocationsDialog.VariableData):
+            return
+
+        location = data.loc
+
+        if location[0] != '%':
+            # no direction was given, grab the one from the wxChoice
+            dir = self.GetSelectedDirection()
+            location = '%' + dir + location
+
+        self.Selection = (location, data.type)
+        self.Destroy()