How To Add Undo For Description Added Dynamically



  • Hi folks,

    I'm still trying to make a python tag plugin which has 2 buttons in tag property. It dynamically adds new descriptions to tag property when "add" button is clicked, and it dynamically removes those descriptions when "remove" button is clicked. So far my code works but I noticed undo doesn't work for these descriptions (see below).

    Is there a way to add undo for descriptions created dynamically? Please tell me how to add undo for that if it's possible for python plugin.
    I tried to insert basic undo things (StartUndo(), AddUndo(), and EndUndo()) into Message() function but it doesn't seem to work.
    My goal is it works like "IK-Spline" does. (see below)

    "Current Status"
    undo_issue.gif

    "My Goal"
    like_ik_spline.gif

    Here's my code.

    test_tag.pyp

    import os
    import c4d
    from c4d import plugins
    
    
    PLUGIN_ID = *******
    LINK_NO = 1100
    FLOAT_NO = 1200
    
    
    class TestTagData(c4d.plugins.TagData):
        def Init(self, node):
            self.controllers_num = 0
            pd = c4d.PriorityData()
            if pd is None:
                raise MemoryError("Failed to create a priority data.")
            pd.SetPriorityValue(lValueID = c4d.PRIORITYVALUE_MODE, data = c4d.CYCLE_EXPRESSION)
            node[c4d.EXPRESSION_PRIORITY] = pd
            return True
    
        def Execute(self, tag, doc, op, bt, priority, flags):
            return c4d.EXECUTIONRESULT_OK
    
        def Message(self, node, type, data):
            if type == c4d.MSG_DESCRIPTION_COMMAND:
                if data["id"][0].id == c4d.TTESTTAG_BUTTON_ADD:
                    doc = c4d.documents.GetActiveDocument()
                    doc.StartUndo()                    #---------- This doesn't seem to work! ----------
                    doc.AddUndo(c4d.UNDOTYPE_CHANGE_NOCHILDREN, node)
                    self.controllers_num += 1
                    doc.EndUndo()                    #----------------------------------------
                    node.SetDirty(c4d.DIRTYFLAGS_DESCRIPTION)
                elif data["id"][0].id == c4d.TTESTTAG_BUTTON_REMOVE:
                    if self.controllers_num > 0:
                        doc = c4d.documents.GetActiveDocument()
                        doc.StartUndo()                    #---------- This doesn't seem to work! ----------
                        doc.AddUndo(c4d.UNDOTYPE_CHANGE_NOCHILDREN, node)
                        self.controllers_num -= 1
                        doc.EndUndo()                    #----------------------------------------
                        node.SetDirty(c4d.DIRTYFLAGS_DESCRIPTION)
            return True
    
        def GetDDescription(self, node, description, flags):
            if not description.LoadDescription(node.GetType()):
                return False
            singleId = description.GetSingleDescID()
            groupId = c4d.DescID(c4d.DescLevel(c4d.ID_TAGPROPERTIES))
            controllers_num = self.controllers_num
            if controllers_num > 0:
                for i in range(controllers_num):
                    linkId = c4d.DescID(c4d.DescLevel(LINK_NO + (i + 1)))
                    if singleId is None or linkId.IsPartOf(singleId)[0]:
                        link_bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BASELISTLINK)
                        link_bc.SetString(c4d.DESC_NAME, "Controller " + str(i + 1))
                        link_bc.SetString(c4d.DESC_SHORT_NAME, "Controller " + str(i + 1))
                        link_bc.SetInt32(c4d.DESC_ANIMATE, c4d.DESC_ANIMATE_ON)
                        if not description.SetParameter(linkId, link_bc, groupId):
                            return False
                    floatId = c4d.DescID(c4d.DescLevel(FLOAT_NO + (i + 1)))
                    if singleId is None or floatId.IsPartOf(singleId)[0]:
                        float_bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
                        float_bc.SetString(c4d.DESC_NAME, "Rate")
                        float_bc.SetString(c4d.DESC_SHORT_NAME, "Rate")
                        float_bc.SetFloat(c4d.DESC_DEFAULT, 1)
                        float_bc.SetInt32(c4d.DESC_ANIMATE, c4d.DESC_ANIMATE_ON)
                        float_bc.SetInt32(c4d.DESC_UNIT, c4d.DESC_UNIT_PERCENT)
                        float_bc.SetFloat(c4d.DESC_MIN, -1000)
                        float_bc.SetFloat(c4d.DESC_MAX, 1000)
                        float_bc.SetFloat(c4d.DESC_MINSLIDER, -1)
                        float_bc.SetFloat(c4d.DESC_MAXSLIDER, 1)
                        float_bc.SetFloat(c4d.DESC_STEP, 0.01)
                        float_bc.SetInt32(c4d.DESC_CUSTOMGUI, c4d.CUSTOMGUI_REALSLIDER)
                        if not description.SetParameter(floatId, float_bc, groupId):
                            return False
                    separatorId = c4d.DescID(0)
                    if singleId is None or separatorId.IsPartOf(singleId)[0]:
                        separator_bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_SEPARATOR)
                        separator_bc.SetInt32(c4d.DESC_CUSTOMGUI, c4d.CUSTOMGUI_SEPARATOR)
                        separator_bc.SetBool(c4d.DESC_SEPARATORLINE, True)
                        if not description.SetParameter(separatorId, separator_bc, groupId):
                            return False
            return (True, flags | c4d.DESCFLAGS_DESC_LOADED)
    
    
    if __name__ == "__main__":
        c4d.plugins.RegisterTagPlugin(id = PLUGIN_ID, str = "Test Tag", info = c4d.TAG_EXPRESSION|c4d.TAG_VISIBLE, g = TestTagData, description = "ttesttag", icon = None)
    

    ttesttag.res

    CONTAINER Ttesttag
    {
        NAME Ttesttag;
        INCLUDE Texpression;
    
        GROUP ID_TAGPROPERTIES
        {
            GROUP TTESTTAG_BUTTONS_GROUP
            {
                COLUMNS 2;
                BUTTON TTESTTAG_BUTTON_ADD { SCALE_H; }
                BUTTON TTESTTAG_BUTTON_REMOVE { SCALE_H; }
            }
        }
    }
    

    ttesttag.h

    #ifndef _TTESTTAG_H_
    #define _TTESTTAG_H_
    
    enum
    {
        TTESTTAG_BUTTONS_GROUP = 1001,
        TTESTTAG_BUTTON_ADD = 1002,
        TTESTTAG_BUTTON_REMOVE = 1003,
    }
    
    #endif
    

    ttesttag.str

    STRINGTABLE Ttesttag
    {
        Ttesttag "Test Tag";
        TTESTTAG_BUTTONS_GROUP "";
        TTESTTAG_BUTTON_ADD "Add";
        TTESTTAG_BUTTON_REMOVE "Remove";
    }
    

    ---------- User Information ----------

    Cinema 4D version: R23
    OS: Windows 10
    Language: Python



  • Just from looking at the code: The description is created dynamically, based on

    self.controllers_num

    This is the only parameter that you change when pressing a button. However, it's an instance attribute of the Python object, and I very much doubt that it is stored with the Undo data in C4D. So any undo action will not restore this attribute, and the description will still be created with the same number of entries.

    If I had the time, I would attempt to store the number of controllers in the BaseContainer of the tag instead so it becomes "visible" to the C4D data that is actually part of the Undo. (No guarantee, I need to walk the dog.)



  • @Cairyn Thank you for your time!

    Hmm, I see. I'm not sure the actual solution for the point you mention but your explanation is so helpful. Anyway, I'll try some workaround on my end.



  • So I tried some changes based on a hint @Cairyn points out, my code seems to work now.
    But I'm still not sure this is a proper way to add undo for dynamic descriptions, so please let me know if you know the correct / better way!

    test_tag.pyp

    import os
    import c4d
    from c4d import plugins
    
    
    PLUGIN_ID = *******
    LINK_NO = 1100
    FLOAT_NO = 1200
    
    
    class TestTagData(c4d.plugins.TagData):
        def Init(self, node):
            self.InitAttr(node, int, c4d.TTESTTAG_CONTROLLERS_NUM)  #-------------------- Changed --------------------
            bc = node.GetDataInstance()  #-------------------- Changed --------------------
            bc.SetInt32(c4d.TTESTTAG_CONTROLLERS_NUM, 0)  #-------------------- Changed --------------------
            pd = c4d.PriorityData()
            if pd is None:
                raise MemoryError("Failed to create a priority data.")
            pd.SetPriorityValue(lValueID = c4d.PRIORITYVALUE_MODE, data = c4d.CYCLE_EXPRESSION)
            node[c4d.EXPRESSION_PRIORITY] = pd
            return True
    
        def Execute(self, tag, doc, op, bt, priority, flags):
            return c4d.EXECUTIONRESULT_OK
    
        def Message(self, node, type, data):
            if type == c4d.MSG_DESCRIPTION_COMMAND:
                if data["id"][0].id == c4d.TTESTTAG_BUTTON_ADD:
                    doc = c4d.documents.GetActiveDocument()
                    doc.StartUndo()
                    doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, node)
                    bc = node.GetDataInstance()  #-------------------- Changed --------------------
                    bc.SetInt32(c4d.TTESTTAG_CONTROLLERS_NUM, bc.GetInt32(c4d.TTESTTAG_CONTROLLERS_NUM) + 1)  #-------------------- Changed --------------------
                    doc.EndUndo()
                elif data["id"][0].id == c4d.TTESTTAG_BUTTON_REMOVE:
                    bc = node.GetDataInstance()  #-------------------- Changed --------------------
                    controllers_num = bc.GetInt32(c4d.TTESTTAG_CONTROLLERS_NUM)  #-------------------- Changed --------------------
                    if controllers_num > 0:  #-------------------- Changed --------------------
                        doc = c4d.documents.GetActiveDocument()
                        doc.StartUndo()
                        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, node)
                        bc.SetInt32(c4d.TTESTTAG_CONTROLLERS_NUM, controllers_num - 1)  #-------------------- Changed --------------------
                        doc.EndUndo()
            return True
    
        def GetDDescription(self, node, description, flags):
            if not description.LoadDescription(node.GetType()):
                return False
            singleId = description.GetSingleDescID()
            groupId = c4d.DescID(c4d.DescLevel(c4d.ID_TAGPROPERTIES))
            bc = node.GetDataInstance()  #-------------------- Changed --------------------
            controllers_num = bc.GetInt32(c4d.TTESTTAG_CONTROLLERS_NUM)  #-------------------- Changed --------------------
            if controllers_num > 0:
                for i in range(controllers_num):
                    linkId = c4d.DescID(c4d.DescLevel(LINK_NO + (i + 1)))
                    if singleId is None or linkId.IsPartOf(singleId)[0]:
                        link_bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BASELISTLINK)
                        link_bc.SetString(c4d.DESC_NAME, "Controller " + str(i + 1))
                        link_bc.SetString(c4d.DESC_SHORT_NAME, "Controller " + str(i + 1))
                        link_bc.SetInt32(c4d.DESC_ANIMATE, c4d.DESC_ANIMATE_ON)
                        if not description.SetParameter(linkId, link_bc, groupId):
                            return False
                    floatId = c4d.DescID(c4d.DescLevel(FLOAT_NO + (i + 1)))
                    if singleId is None or floatId.IsPartOf(singleId)[0]:
                        float_bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
                        float_bc.SetString(c4d.DESC_NAME, "Rate")
                        float_bc.SetString(c4d.DESC_SHORT_NAME, "Rate")
                        float_bc.SetFloat(c4d.DESC_DEFAULT, 1)
                        float_bc.SetInt32(c4d.DESC_ANIMATE, c4d.DESC_ANIMATE_ON)
                        float_bc.SetInt32(c4d.DESC_UNIT, c4d.DESC_UNIT_PERCENT)
                        float_bc.SetFloat(c4d.DESC_MIN, -1000)
                        float_bc.SetFloat(c4d.DESC_MAX, 1000)
                        float_bc.SetFloat(c4d.DESC_MINSLIDER, -1)
                        float_bc.SetFloat(c4d.DESC_MAXSLIDER, 1)
                        float_bc.SetFloat(c4d.DESC_STEP, 0.01)
                        float_bc.SetInt32(c4d.DESC_CUSTOMGUI, c4d.CUSTOMGUI_REALSLIDER)
                        if not description.SetParameter(floatId, float_bc, groupId):
                            return False
                    separatorId = c4d.DescID(0)
                    if singleId is None or separatorId.IsPartOf(singleId)[0]:
                        separator_bc = c4d.GetCustomDataTypeDefault(c4d.DTYPE_SEPARATOR)
                        separator_bc.SetInt32(c4d.DESC_CUSTOMGUI, c4d.CUSTOMGUI_SEPARATOR)
                        separator_bc.SetBool(c4d.DESC_SEPARATORLINE, True)
                        if not description.SetParameter(separatorId, separator_bc, groupId):
                            return False
            return (True, flags | c4d.DESCFLAGS_DESC_LOADED)
    
    
    if __name__ == "__main__":
        c4d.plugins.RegisterTagPlugin(id = PLUGIN_ID, str = "Test Tag", info = c4d.TAG_EXPRESSION|c4d.TAG_VISIBLE, g = TestTagData, description = "ttesttag", icon = None)
    

    ttesttag.res

    CONTAINER Ttesttag
    {
        NAME Ttesttag;
        INCLUDE Texpression;
    
        GROUP ID_TAGPROPERTIES
        {
            GROUP TTESTTAG_BUTTONS_GROUP
            {
                COLUMNS 2;
                BUTTON TTESTTAG_BUTTON_ADD { SCALE_H; }
                BUTTON TTESTTAG_BUTTON_REMOVE { SCALE_H; }
                LONG TTESTTAG_CONTROLLERS_NUM { HIDDEN; ANIM OFF; MIN 0; MAX 10000; }  //-------------------- Added --------------------
            }
        }
    }
    

    ttesttag.h

    #ifndef _TTESTTAG_H_
    #define _TTESTTAG_H_
    
    enum
    {
        TTESTTAG_BUTTONS_GROUP = 1001,
        TTESTTAG_BUTTON_ADD = 1002,
        TTESTTAG_BUTTON_REMOVE = 1003,
        TTESTTAG_CONTROLLERS_NUM = 1004,  //-------------------- Added --------------------
    }
    
    #endif
    

    ttesttag.str

    STRINGTABLE Ttesttag
    {
        Ttesttag "Test Tag";
        TTESTTAG_BUTTONS_GROUP "";
        TTESTTAG_BUTTON_ADD "Add";
        TTESTTAG_BUTTON_REMOVE "Remove";
        TTESTTAG_CONTROLLERS_NUM "";  //-------------------- Added --------------------
    }
    


  • hi,

    as @Cairyn said, when you add the undo to the stack, it does store a copy of your object.

    The "problem" is that your objet is storing your data in the instance itself. Cinema4D doesn't know anything about it.

    Two solution, either you store this value (controllers_num) in a BaseContainer so cinema4D will copy the basecontainer.

    Either you implement CopyTo to make cinema4D aware on how to handle that data.
    As said in the documentation, if you implement the CopyTo, you also need to implement Read and Write functions (used when you save or load a c4d file)

       def CopyTo(self, dest, snode, dnode, flags, trn):
            dest.controllers_num = self.controllers_num
            return True
    

    Or you just store the data in the BaseContainer of the object and c4d will manage it for you.

    Cheers,
    Manuel.



  • @m_magalhaes Thank you for your explanation! It's really helpful to know several solutions for a problem.
    C4D python doc is not user friendly for newbies like me but this community is super friendly and pretty awesome! 🤛



  • @beatgram Just for good style, it is not necessary to add a LONG to the .res file. You can store a value in the BaseContainer without making it accessible in the GUI. The decisive thing is just that C4D knows about the value. Which can be achieved by writing it into the BaseContainer (which is a C4D data structure and "known" by default), or by explicitly handling it by implementing the CopyTo, Read, Write function group.



  • @Cairyn Thank you so much for helping me again! 😊


Log in to reply