Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    How To Add Undo For Description Added Dynamically

    Cinema 4D SDK
    python r23
    3
    8
    1.1k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • beatgramB
      beatgram
      last edited by beatgram

      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

      1 Reply Last reply Reply Quote 0
      • CairynC
        Cairyn
        last edited by

        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.)

        beatgramB 2 Replies Last reply Reply Quote 0
        • beatgramB
          beatgram @Cairyn
          last edited by

          @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.

          1 Reply Last reply Reply Quote 0
          • beatgramB
            beatgram @Cairyn
            last edited by

            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 --------------------
            }
            
            CairynC 1 Reply Last reply Reply Quote 0
            • ManuelM
              Manuel
              last edited by

              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.

              MAXON SDK Specialist

              MAXON Registered Developer

              beatgramB 1 Reply Last reply Reply Quote 1
              • beatgramB
                beatgram @Manuel
                last edited by

                @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! 🤛

                1 Reply Last reply Reply Quote 0
                • CairynC
                  Cairyn @beatgram
                  last edited by

                  @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.

                  beatgramB 1 Reply Last reply Reply Quote 1
                  • beatgramB
                    beatgram @Cairyn
                    last edited by

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

                    1 Reply Last reply Reply Quote 0
                    • First post
                      Last post