Set TreeView values dynamically

On 12/04/2018 at 08:21, xxxxxxxx wrote:

Hi everyone,

I was having a bit of experimentation with the TreeView custom GUI, which btw is preety awesome, but having some difficulty in setting the values correctly to each column.

So will explain the ideas/problems that I have which hopefully someone will be able to help.

Process 1: You add the selected objects to a list, by pressing the "Add" button.

Process 2:  Each selected object and values populate the list, in the correct column.
Issue 2: When I create a list, can't distribute the values correctly to each column. As per the final result example above.

It may be related with some of the code below (taken from a good example by Maxime: https://plugincafe.maxon.net/topic/10654/14102_using-customgui-listview&KW=tree&PID=56287#56287)

I can see that only works for one value, but would like set the 3 values.

    
    
    class PickObjs(object) :
    
        objSel = ""
        _selected = False
    
        def __init__(self, objSel) :
    
            self.objSel = objSel
            print self.objSel <-- PRINTS CORRECTLY THE OBJECT AND VALUES!
    
        @property
        def IsSelected(self) :
            return self._selected
    
        def Select(self) :
            self._selected = True
    
        def Deselect(self) :
            self._selected = False
    
        def __repr__(self) :
            return str(self)
    
        def __str__(self) :
            return self.objSel <--- CAN ONLY RETURN A STRING AT THE TIME?

Process 3: When changing the ComboBox value, change the axis value in list for check marked objects.
Issue 3:  Need to have the values in the correct place and access it to make the change. Not sure how this would be done?

Hopefully this is comprehensive, but please let me know if don't know what I'm talking about :slightly_smiling_face:.

Thank you in advance!

Andre

On 13/04/2018 at 04:58, xxxxxxxx wrote:

Hi Andre,

we expect the issue rather in your implementation of the TreeViewFunctions. Maybe you can show us some code of these?

On 13/04/2018 at 05:31, xxxxxxxx wrote:

Hi Andreas,

Yeah sure! Let me know if you need anything else!

# Global Variables
PLUGIN_VERSION = 'v1.0'
PLUGIN_ID = 1000010 #TESTING ID
GRP_COMBOXFILES = 1002
GRP_COMBOAXIS = 1003
GRP_ADDBTN = 1004
GRP_AUTOBTN = 1005
GRP_RUNBTN = 1006
OBJLIST = 1021
STATUS = 1031
INITIAL_WIDTH = 100
INITIAL_HEIGHT = 20
TREE_CHECK = 1
TREE_OBJ = 2
TREE_AXIS = 3
TREE_FUNC = 4
PATH = storage.GeGetStartupWritePath() + '/plugins/AO_Tools/eXpressoMachine/res/modules/'
XNODESPATH = sys.path.append(storage.GeGetStartupWritePath() + '/plugins/AO_Tools/eXpressoMaker/res/modules')
MAINBC = c4d.BaseContainer()
##################
  
# Class which represent an object, aka an item in our list
class PickObjs(object) :
  
    objSel = ""
    _selected = False
  
    def __init__(self, objSel) :
  
        self.objSel = objSel
        print self.objSel
  
    @property
    def IsSelected(self) :
        return self._selected
  
    def Select(self) :
        self._selected = True
  
    def Deselect(self) :
        self._selected = False
  
    def __repr__(self) :
        return str(self)
  
    def __str__(self) :
        return self.objSel
  
  
class ListView(c4d.gui.TreeViewFunctions) :
  
    def __init__(self) :
        self.listOfObjs = list() # Store all objects we need to display in this list
        
    def IsResizeColAllowed(self, root, userdata, lColID) :
        return True
  
    def IsTristate(self, root, userdata) :
        return False
        
    def changeAxis(self, obj) :
        print obj
        
    # The user is allowed to move all columns.
    # TREEVIEW_MOVE_COLUMN must be set in the container of AddCustomGui.
    def IsMoveColAllowed(self, root, userdata, lColID) :
  
        return False
        
    # Return the first element in the hierarchy, or None if there is no element.
    def GetFirst(self, root, userdata) :
  
        rValue = None if not self.listOfObjs else self.listOfObjs[0]
        return rValue
        
    # Return a child of a node, since we only want a list, we return None everytime
    def GetDown(self, root, userdata, obj) :
  
        return None
        
    # Returns the next Object to display after arg:'obj'
    def GetNext(self, root, userdata, obj) :
  
        rValue = None
        currentObjIndex = self.listOfObjs.index(obj)
        nextIndex = currentObjIndex + 1
        if nextIndex < len(self.listOfObjs) :
            rValue = self.listOfObjs[nextIndex]
  
        return rValue
        
    # Returns the previous Object to display before arg:'obj'
    def GetPred(self, root, userdata, obj) :
  
        rValue = None
        currentObjIndex = self.listOfObjs.index(obj)
        predIndex = currentObjIndex - 1
        if 0 <= predIndex < len(self.listOfObjs) :
            rValue = self.listOfObjs[predIndex]
  
        return rValue
        
    # Return a unique ID for the element in the TreeView.
    def GetId(self, root, userdata, obj) :
  
        return hash(obj)
        
    # Called when the user selects an element.
    def Select(self, root, userdata, obj, mode) :
  
        if mode == c4d.SELECTION_NEW:
            for tex in self.listOfObjs:
                tex.Deselect()
            obj.Select()
        elif mode == c4d.SELECTION_ADD:
            obj.Select()
        elif mode == c4d.SELECTION_SUB:
            obj.Deselect()
  
    def IsSelected(self, root, userdata, obj) :
        """
        Returns: True if *obj* is selected, False if not.
        """
        return obj.IsSelected
        
    # Called when the user clicks on a checkbox for an object in a c4d.LV_CHECKBOX` column.
    def SetCheck(self, root, userdata, obj, column, checked, msg) :
  
        if checked:
        
            obj.Select()
            
        else:
        
            obj.Deselect()
            
    # Returns: (int) : Status of the checkbox in the specified *column* for *obj*.
    def IsChecked(self, root, userdata, obj, column) :
  
        if obj.IsSelected:
    
            return c4d.LV_CHECKBOX_CHECKED | c4d.LV_CHECKBOX_ENABLED
            
        else:
        
            return c4d.LV_CHECKBOX_ENABLED
            
    # Returns the name to display for arg:'obj', only called for column of type LV_TREE
    def GetName(self, root, userdata, obj) :
  
        return str(obj)
        
    # Create all context menu values (Mouse Right Click)
    def CreateContextMenu(self, root, userdata, obj, lColumn, bc) :
        
        bc.RemoveData(900001) # Remove all option
    
    # Draw into a Cell, only called for column of type LV_USER
    def DrawCell(self, root, userdata, obj, col, drawinfo, bgColor) :
  
        if col == TREE_OBJ:
        
            name = obj.GetName()
            geUserArea = drawinfo["frame"]
            w = geUserArea.DrawGetTextWidth(name)
            h = geUserArea.DrawGetFontHeight()
            xpos = drawinfo["xpos"]
            ypos = drawinfo["ypos"] + drawinfo["height"]
            drawinfo["frame"].DrawText(name, xpos, ypos - h * 1.1)
            
        elif col == TREE_AXIS:
        
            name = obj.GetName()
            geUserArea = drawinfo["frame"]
            w = geUserArea.DrawGetTextWidth(name)
            h = geUserArea.DrawGetFontHeight()
            xpos = drawinfo["xpos"]
            ypos = drawinfo["ypos"] + drawinfo["height"]
            drawinfo["frame"].DrawText(name, xpos, ypos - h * 1.1)
        
        elif col == TREE_FUNC:
        
            name = obj.GetName()
            geUserArea = drawinfo["frame"]
            w = geUserArea.DrawGetTextWidth(name)
            h = geUserArea.DrawGetFontHeight()
            xpos = drawinfo["xpos"]
            ypos = drawinfo["ypos"] + drawinfo["height"]
            drawinfo["frame"].DrawText(name, xpos, ypos - h * 1.1)
            
    # Called when a delete event is received.
    def DeletePressed(self, root, userdata) :
  
        for obj in reversed(self.listOfObjs) :
        
            if obj.IsSelected:
            
                self.listOfObjs.remove(obj)
                                
# Main class for the plugin dialog (UI).
class eXpressoMakerDialog(gui.GeDialog) :
  
    _treegui = None # Our CustomGui TreeView
    _listView = ListView() # Our Instance of c4d.gui.TreeViewFunctions
    
    # Create the dialog's layout
    def CreateLayout(self) :
    
        self.SetTitle('eXpresso Machine ' + PLUGIN_VERSION) # Title name
  
        ################### Main group container for all the widgets.
        self.GroupBegin(10000, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT, 1, 1, 'MainGroup')
        
        ################ Combo box that gets the sub-modules from the modules folder.
        self.GroupBegin(10001, c4d.BFH_SCALEFIT, 1, 1, 'Function')
        self.GroupBorder(c4d.BORDER_GROUP_OUT)
        self.GroupBorderSpace(10, 10, 10, 10)
        self.AddComboBox(GRP_COMBOXFILES, c4d.BFH_SCALEFIT, INITIAL_WIDTH, INITIAL_HEIGHT)
        
        # Get all the modules in list and add it as a child of the combo box.
        filesSubID = 0
        
        for file in listFiles() :
            self.AddChild(GRP_COMBOXFILES, filesSubID, file)
            filesSubID += 1
  
        self.GroupEnd()
        ################
        
        ################ Selection group container.
        self.GroupBegin(10002, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT, 2, 1, 'Selection')
        self.GroupBorder(c4d.BORDER_GROUP_OUT)
        self.GroupBorderSpace(10, 10, 10, 10)
        
        ############# Main menu buttons group container.
        self.GroupBegin(10020, c4d.BFV_SCALEFIT, 1, 1, 'Menu')
        
        ########## Axis layout group container, combo box and button.
        self.GroupBegin(10021, c4d.BFH_SCALEFIT, 1, 1, 'Axis')
        self.GroupBorder(c4d.BORDER_GROUP_OUT)
        self.GroupBorderSpace(10, 10, 10, 10)
        self.AddComboBox(GRP_COMBOAXIS, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT, 30, INITIAL_HEIGHT)     
  
        # Get all the axis in list and add it as a child of the combo box.
        axisID = 0
        global axisList
        axisList = ['X', 'Y', 'Z']
        
        for axis in axisList:
            self.AddChild(GRP_COMBOAXIS, axisID, axis)
            axisID += 1
            
        self.GroupEnd()    
        ##########
        
        ########## Objects layout group container and function buttons.
        self.GroupBegin(10022, c4d.BFH_SCALEFIT, 1, 1, 'Objects')
        self.GroupBorder(c4d.BORDER_GROUP_OUT)
        self.GroupBorderSpace(10, 10, 10, 10)
        self.AddButton(GRP_AUTOBTN, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT, INITIAL_WIDTH, INITIAL_HEIGHT, 'Auto-Find')
        self.AddButton(GRP_ADDBTN, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT, INITIAL_WIDTH, INITIAL_HEIGHT, 'Add')
        self.GroupEnd()
        ##########
        
        self.GroupEnd()
        #############
        
        ############# Object TreeView container, where list of object, axis and function selection will be displayed. Each line is selectable and mutable.
        self.GroupBegin(10023, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT, 1, 1, 'Object List')
        
        treeViewGUI = c4d.BaseContainer()
        treeViewGUI.SetBool(c4d.TREEVIEW_BORDER, c4d.BORDER_THIN_IN)
        treeViewGUI.SetBool(c4d.TREEVIEW_BORDER, True)
        treeViewGUI.SetBool(c4d.TREEVIEW_HAS_HEADER, True)
        treeViewGUI.SetBool(c4d.TREEVIEW_HIDE_LINES, True)
        treeViewGUI.SetBool(c4d.TREEVIEW_MOVE_COLUMN, True)
        treeViewGUI.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True)
        treeViewGUI.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True)
        treeViewGUI.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True)
  
        self._treegui = self.AddCustomGui(OBJLIST, c4d.CUSTOMGUI_TREEVIEW, "", c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 400, 0, treeViewGUI)
        self.GroupEnd()
        #############
        
        self.GroupEnd()
        ################
        
        ################ Status group container, where informs the user about the function actions.
        self.GroupBegin(10003, c4d.BFH_SCALEFIT, 1, 1)
        self.AddStaticText(STATUS, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT)
        self.GroupEnd()
        ################
        
        ################ Run group container that includes the main button to run all of the list functions.
        self.GroupBegin(10004, c4d.BFH_SCALEFIT, 1, 1)
        self.AddSeparatorV(INITIAL_WIDTH, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT)        
        self.AddButton(GRP_RUNBTN, c4d.BFV_SCALEFIT | c4d.BFH_SCALEFIT, INITIAL_WIDTH, INITIAL_HEIGHT, 'Run')
        self.GroupEnd()
        ################
  
        self.GroupEnd()
        ###################
        
        return True
        
    # Initialize default values
    def InitValues(self) :
    
        # Initialize the column layout for the TreeView.
        layout = c4d.BaseContainer()
        layout.SetLong(TREE_CHECK, c4d.LV_CHECKBOX)
        layout.SetLong(TREE_OBJ, c4d.LV_TREE)
        layout.SetLong(TREE_AXIS, c4d.LV_USER)
        layout.SetLong(TREE_FUNC, c4d.LV_USER)
        self._treegui.SetLayout(TREE_FUNC, layout)
  
        # Set the header titles.
        self._treegui.SetHeaderText(TREE_CHECK, "")
        self._treegui.SetHeaderText(TREE_OBJ, "Object")
        self._treegui.SetHeaderText(TREE_AXIS, "Axis")
        self._treegui.SetHeaderText(TREE_FUNC, "Function")
        self._treegui.Refresh()
  
        # Set TreeViewFunctions instance used by our CUSTOMGUI_TREEVIEW
        self._treegui.SetRoot(self._treegui, self._listView, None)
        return True
                
     # Iterate through all the scene objects that include the words 'PIVOT' and 'MOVE' and append it to a list.
    def getObjs(self, op, output) : 
        
        while op:
        
            if 'PIVOT' in op.GetName().upper() or 'MOVE' in op.GetName().upper() :
                                 
                output.append(op)
                    
            self.getObjs(op.GetDown(),output)
            op = op.GetNext()
            
        return output
    
    # Return the correct ID for the axis selected.
    def getAxis(self, axis) :
    
        selAxis = {'X' : 'c4d.VECTOR_X', # ID: 1000
                'Y' : 'c4d.VECTOR_Y', # ID: 1001
                'Z' : 'c4d.VECTOR_Z'} # ID: 1002
                
        return selAxis[axis]
        
    '''NOT USED 
    def CoreMessage(self, id, msg) :
           
        return gui.GeDialog.CoreMessage(self, id, msg)
        '''
        
    # Built-In function to run events when triggered by the dialog widgets.
    def Command(self, id, msg) :
    
        fileSelectedIndex = self.GetInt32(GRP_COMBOXFILES) # Get the function combo box index for the value selected.
        axisSelectedIndex = self.GetInt32(GRP_COMBOAXIS) # Get the axis combo box index for the value selected.
        
        doc = c4d.documents.GetActiveDocument() # Get active document.
        
        # Event trigger for the axis combo box and button.
        if (id == GRP_COMBOAXIS) :
  
            if self._listView.listOfObjs != []:
                i = 0
                for obj in self._listView.listOfObjs:
  
                    print self._listView.changeAxis(obj)
                
                self.SetString(STATUS, 'Axis changed!') # Event run, show the event message to the user.
                
            else:
            
                self.SetString(STATUS, 'No objects to change the axis!') # If no objects are on list, show the event message to the user.
                
        # Event trigger for the add button.
        if (id == GRP_ADDBTN) :
  
            selObjs = doc.GetActiveObjects(0) # Get a list of selected objects.
                        
            if selObjs != []:
            
                # Add data to our DataStructure (ListView)
                for obj in selObjs:
                    newID = len(self._listView.listOfObjs) + 1 
                    objName = obj.GetName()
                    axisName = axisList[axisSelectedIndex].upper()
                    funcName = str(files[fileSelectedIndex])
                    
                    self._listView.listOfObjs.append([objName, axisName, funcName])
                    for objSel in self._listView.listOfObjs:
                    
                        for value in objSel:
                        
                            PickObjs(value)
                    
                # Refresh the TreeView
                self._treegui.Refresh()
                
                self.SetString(STATUS, 'Object(s) added!') # Event run, show the event message to the user.
                
            else:
  
                self.SetString(STATUS, 'No objects selected!') # If no objects are on list, show the event message to the user.
               
        if (id == GRP_AUTOBTN) :
        
            parentObjs = doc.GetObjects() # Get a list of all parent objects of the scene.
            selObjs = []
            
            if parentObjs != []:
                
                # Get all the objects that have the word 'PIVOT' or 'MOVE' on its name.
                # Case sensitivity is not a a problem, as the function converts the name to upper case.
                self.getObjs(parentObjs[0], selObjs)
                
                if selObjs != []:
                
                    # Add data to our DataStructure (ListView)
                    for obj in selObjs:
                        newID = len(self._listView.listOfObjs) + 1 
                        objName = PickObjs(obj.GetName(), axisList[axisSelectedIndex].upper(), str(files[fileSelectedIndex]))
                        self._listView.listOfObjs.append(objName)
                        
                    # Refresh the TreeView
                    self._treegui.Refresh()
                    
                    self.SetString(STATUS, 'Object(s) Added') # Event run, show the event message to the user.
                    
                else:
                
                    self.SetString(STATUS, 'Could not find any objects!') # If no objects are on list, show the event message to the user.
                
            else:
        
                self.SetString(STATUS, 'Could not find any objects!') # If no objects are on list, show the event message to the user.
                
        if (id == GRP_COMBOXFILES) :
        
            prevMsg = self.GetString(OBJLIST) # Get the list (message) from the multi-line widget.
  
            if prevMsg != '':       
        
                self.SetString(STATUS, 'Function changed to ' + str(files[fileSelectedIndex])) # Event run, show the event message to the user.
            
            else:
            
                self.SetString(STATUS, 'No objects selected!') # If no objects are on list, show the event message to the user.
                
        if (id == GRP_RUNBTN) :
        
            prevMsg = self.GetString(OBJLIST) # Get the list (message) from the multi-line widget.
  
            if prevMsg != '':
        
                with localimport('res/modules') :
                
                    lines = prevMsg.split('\n') # Split the line by the new line character into different sections.
                    del lines[-1] # Delete the last empty line.
                    lineIndex = 0
  
                    for line in lines:
                    
                        wordList = line.split() # Split the line into different sections.
                        
                        # Freeze the transformations for of the object.
                        for id, value in MAINBC:
                            
                            if id == lineIndex:
  
                                 objBC = MAINBC.GetData(id)
                                 
                                 for id, value in objBC:
                                    print id, value
                        
                        if wordList[-1] not in sys.modules:
                        
                            __import__(wordList[-1]) # Call the correct model to run.
                            
                        else:
                        
                            reload(__import__(wordList[-1])) # If the module modified, reload the model and run it.
                            
                        lineIndex += 1
                        
                    self.SetString(STATUS, 'Rig done!') # Event run, show the event message to the user.
                    
            else:
            
                self.SetString(STATUS, 'No objects selected!') # If no objects are on list, show the event message to the user.
  
        return True
    
# Return the relevant list of objects that contain the file extension '.py'
# and do not start with the file name '__innit__'
def listFiles() :
  
    global files
    files = []
  
    if os.path.exists(PATH) :
  
        for file in os.listdir(PATH) :
        
            if file.endswith('.py') and os.path.splitext(file)[0] != '__init__':
        
                files.append(os.path.splitext(file)[0])
    
    return files
    
# Open the plugin in a new window when the main plugin function is called
class eXpressoMakerPlugin(plugins.CommandData) :
  
    dialog = None
    
    # Creates the dialog
    def Execute(self, doc) :
    
        if self.dialog is None:
            self.dialog = eXpressoMakerDialog()
         
        return self.dialog.Open(c4d.DLG_TYPE_ASYNC, PLUGIN_ID, defaultw=300, defaulth=150, xpos=-1, ypos=-1)
    
    # Manages the dialog
    def RestoreLayout(self, sec_ref) :
  
        if self.dialog is None:
            self.dialog = eXpressoMakerDialog()
         
        return self.dialog.Restore(PLUGIN_ID, secret=sec_ref)
        
if __name__ == '__main__':
  
    path, fn = os.path.split(__file__)
    bmp = bitmaps.BaseBitmap() # We need an instance of BaseBitmap class to use an image
    bmp.InitWith(os.path.join(path, 'res/icons/', 'icon.tif')) # The location where the menu image exists
     
    okyn = plugins.RegisterCommandPlugin(PLUGIN_ID, 'eXpresso Machine ' + PLUGIN_VERSION, 0, bmp, 'eXpresso Machine ' + PLUGIN_VERSION, eXpressoMakerPlugin())
    
    if (okyn) :
        print 'eXpresso Machine ' + PLUGIN_VERSION + ' Loaded!'
        c4d.StatusSetText('eXpresso Machine ' + PLUGIN_VERSION + ' Loaded!')

Thank you very much Andreas! :slightly_smiling_face:

On 18/04/2018 at 09:23, xxxxxxxx wrote:

Hi Andre,

terribly sorry, I need to ask for a bit more patience. I'll get back to you as soon as possible. Hopefully still this week.

On 19/04/2018 at 00:51, xxxxxxxx wrote:

Hi Andreas,

No need to apologise! I appreciate your help! :slightly_smiling_face:

Thank you very much again!

On 20/04/2018 at 06:07, xxxxxxxx wrote:

Hi Andre,

I think, the main problem of your code is, that you are actually storing tuples in the tree content list, not "PickObjs" (as all the rest of your code assumes). This causes all kinds of problems all over the place. This you will need to either debug yourself or ask someone in our community for help.

This is also the reason, why you get the entire tuple shown in the first column.

GetName() delivers only the content of the LV_TREE column (basically the main column).
All LV_USER columns you need to draw yourself in DrawCell(). Which again doesn't work in your code, because the tuples stored in the list have no GetName() function. Also DrawCell() needs to draw only the LV_USER columns, no need to take care of TREE_OBJ in there.

One general advice:
Pay attention to any errors on the Console. These can usually get you already a long way.

I hope, this helps to get you started.

On 23/04/2018 at 05:55, xxxxxxxx wrote:

Hi Andreas,

Thank you immensely for your help!

I've managed to correct the code and make it do what I wanted, with your advice, but (apologies if I didn't understand) how can I access the the data for each "cell"? I will get the LV_TREE obj but not the LV_USER data.

I understand that the list is only appending the LV_TREE obj and not the other two, because it is returned by the function.
My question would be how to attach the other LV_USER values to the same list, so I can use it in a later stage?
If this is not possible, should I create a new list/baseContainer?

Thank you so much!

Andre

On 23/04/2018 at 08:12, xxxxxxxx wrote:

Hi Andre,

I'm not sure I understand your question. if I fail to answer your question, please don't hesitate to ask again.

You need to differentiate two things. One is your data (i.e. a linked list of objects or an array of your PickObjects) and the other is the display in the TreeView.

The tree doesn't care for your data, but instead you need to tell it, what to display in the LV_TREE column (via GetName()) and for all other columns (via DrawCell()). In either function you get the obj parameter, which from the tree point of view is just an opaque reference (it's not used inside by the tree), which provides you with the means to pick whatever needed from your data.

Or to answer your question "how can I access the the data for each cell?" more directly: You don't. The tree does not store any data, it just displays your data.

Similar in the other direction. For example on left click onto the Axis column, you want to have a popup with axis options. Well, you again get the obj reference in MouseDown(). Then you can for example use ShowPopupDialog() and with the result you'd update your data or the entities referred to by your data. Of course you could achieve similar by using the tree's context menu directly (CreateContextMenu()).

Unfortunately here's also a small limitation of the Python API. The C++ API has PopupEditText(), which would allow for overlay text editing, roughly as shown in your screenshot, but that's currently missing in the Python API.

On 24/04/2018 at 01:06, xxxxxxxx wrote:

Hi Andreas,

You did answer it and you also gave me some alternative and ideas to use.
Thank you so much! It makes more sense, now.
I'm still getting the grips with Python, but it seems that C++ is the way to go as well.

Appreciate your patience and help! :slightly_smiling_face:

Have a great day!

Andre