Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush Python 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

    Undo BaseDocument Container Changes?

    PYTHON Development
    0
    7
    531
    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.
    • H
      Helper
      last edited by

      On 05/05/2014 at 17:34, xxxxxxxx wrote:

      Is it possible to add changes to a BaseDocument's container to that document's undo buffer? Based on some preliminary searches of the forum, it doesn't seem like there's a method.

      So I guess my question is, where should my dialog plugin store its data so that the user can undo/redo changes in the dialog and have the dialog state get stored with the document?

      I've tried storing data in the LayerObjectRoot but that results in a hard-crash as it seems to only support GetUp/Down/etc. and not other BaseList2D functions.

      All other locations: objects, materials, render settings, layers - can be accessed and deleted by the user. Something I definitely don't want to happen.
      Thanks!

      Donovan

      1 Reply Last reply Reply Quote 0
      • H
        Helper
        last edited by

        On 06/05/2014 at 08:09, xxxxxxxx wrote:

        Hi Donovan,

        Waiting for a developer reply (see my answer to your reply on another thread here). But I
        don't think its possible.

        I do think however that it is absolutely acceptable to exploit something that is always available
        in a document, such as the basic SceneHooks. "BaseSettings Hooks" sounds pretty good, too. 😄
        I included this technique in your task-list example in the Python SDK.

        https://github.com/PluginCafe/py-cinema4dsdk/blob/e9497d3739ef78b56c71e43adbaeb7457eef78e6/gui/task-list.pyp#L221-L233

                # Save to the document.
                doc = self._last_doc
                hook = GetBaseSettingsHook(doc)
                if hook:
          
                    # No matter what happens, the undo step must be
                    # closed after it was started.
                    doc.StartUndo()
                    try:
                        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, hook)
                        hook.GetDataInstance().SetContainer(PLUGIN_ID, bc)
                    finally:
                        doc.EndUndo()
        

        -Niklas

        1 Reply Last reply Reply Quote 0
        • H
          Helper
          last edited by

          On 06/05/2014 at 13:25, xxxxxxxx wrote:

          Can you please post a simpler example?
          You've got methods calling other methods. And it's very hard to follow what's going on with the SceneHook code.

          Here's a much simpler example of storing values in a container.
          But the container does not undo when the undo is used:

          PLUGIN_ID = 1000009  # Testing id ONLY!!!!!!!   
          MY_BTN1 = 10001  
          MY_BTN2 = 10002  
            
          class MyDialog(gui.GeDialog) :  
            
            mybc = c4d.BaseContainer()  
            
            def CreateLayout(self) :  
                self.AddButton(MY_BTN1, c4d.BFH_CENTER, 100, 10, name="Set Container")   
                self.AddButton(MY_BTN2, c4d.BFH_CENTER, 100, 10, name="Read Container")           
                return True  
            
            def InitValues(self) :  
                return True  
            
            def Command(self, id, msg) :  
              
                doc = c4d.documents.GetActiveDocument()  
              
                #Store value in a container when this button is pressed  
                if id == MY_BTN1:  
                    hook = doc.FindSceneHook(c4d.ID_BS_HOOK)  
                    bc = hook.GetDataInstance()  
            
                    doc.StartUndo()  
                    doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, hook)      
                    doc[123456] = self.mybc  
                    self.mybc.SetString(1, "hello")              
                    hook.GetDataInstance().SetContainer(PLUGIN_ID, bc)              
                    doc.EndUndo()  
                      
                #Read the container when this button is pressed  
                if id == MY_BTN2:          
                    print len(self.mybc)  
                    print self.mybc.GetString(1)  
                      
                return True  
              
            
          class myDialog_Main(plugins.CommandData) :  
            dialog = None  
              
            def Execute(self, doc) :  
                #Create the dialog  
                if self.dialog is None:  
                    self.dialog = MyDialog()  
                return self.dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=PLUGIN_ID, defaultw=200, defaulth=150, xpos=-1, ypos=-1)  
                  
          if __name__ == "__main__":  
            plugins.RegisterCommandPlugin(PLUGIN_ID, "myPythonDialog",0,None,"", myDialog_Main())
          

          -ScottA

          1 Reply Last reply Reply Quote 0
          • H
            Helper
            last edited by

            On 08/05/2014 at 02:37, xxxxxxxx wrote:

            The container you got there won't change. You have to read it back from the hook.
            BaseContainer.SetContainer(sub_bc) will make a copy of sub_bc. You could use
            .GetContainerInstance() to get the actually stored container afterwards but the undo-process
            will create a new copy as well.

            import c4d
            from c4d import gui, plugins
              
            PLUGIN_ID = 1000009  # Testing id ONLY!!!!!!!
            MY_BTN1 = 10001
            MY_BTN2 = 10002
              
            class MyDialog(gui.GeDialog) :
              
                mybc = c4d.BaseContainer()
              
                def CreateLayout(self) :
                    self.AddButton(MY_BTN1, c4d.BFH_CENTER, 100, 10, name="Set Container")
                    self.AddButton(MY_BTN2, c4d.BFH_CENTER, 100, 10, name="Read Container")
                    return True
              
                def InitValues(self) :
                    return True
              
                def Command(self, id, msg) :
              
                    doc = c4d.documents.GetActiveDocument()
              
                    #Store value in a container when this button is pressed
                    if id == MY_BTN1:
                        hook = doc.FindSceneHook(c4d.ID_BS_HOOK)
                        bc = hook.GetDataInstance()
              
                        doc.StartUndo()
                        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, hook)
                        doc[123456] = self.mybc
                        self.mybc.SetString(1, "hello")
                        hook.GetDataInstance().SetContainer(PLUGIN_ID, bc)
                        doc.EndUndo()
              
                    #Read the container when this button is pressed
                    if id == MY_BTN2:
                        hook = doc.FindSceneHook(c4d.ID_BS_HOOK)
                        self.mybc = hook.GetDataInstance().GetContainer(PLUGIN_ID)
                        print len(self.mybc)
                        print self.mybc.GetString(1)
              
                    return True
              
              
            class myDialog_Main(plugins.CommandData) :
                dialog = None
              
                def Execute(self, doc) :
                    #Create the dialog
                    if self.dialog is None:
                        self.dialog = MyDialog()
                    return self.dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=PLUGIN_ID, defaultw=200, defaulth=150, xpos=-1, ypos=-1)
              
            if __name__ == "__main__":
                plugins.RegisterCommandPlugin(PLUGIN_ID, "myPythonDialog",0,None,"", myDialog_Main())
            

            -Niklas

            1 Reply Last reply Reply Quote 0
            • H
              Helper
              last edited by

              On 08/05/2014 at 08:21, xxxxxxxx wrote:

              Thanks Niklas.
              But the code you posted does not undo the container when the document is undone.
              However...It does undo the container when the "Read Container" button is pressed. Because you're setting the custom container mybc = the document's container like this:
              self.mybc = hook.GetDataInstance().GetContainer(PLUGIN_ID)

              What you have created is a container undo button.
              But undoing the actual document still doesn't undo the container.

              Is there a way to make this happen: self.mybc = hook.GetDataInstance().GetContainer(PLUGIN_ID)
              When the document is undone (Ctrl+Z)?

              -ScottA

              1 Reply Last reply Reply Quote 0
              • H
                Helper
                last edited by

                On 08/05/2014 at 14:23, xxxxxxxx wrote:

                @Niklas: Thanks for the SceneHook suggestion and code example - that definitely points me in the right direction.

                @Scott: I think the reason your undo isn't working is because you're saving it to the document's container directly. The doc object doesn't support undo. Try saving it to the hook's container.

                I've created a slightly more complex example based on yours which seems to be working:

                  
                """This is a simple example of how to add undo support for changes saved into a document's container.   
                This is best used when you have a dialog plugin that needs to save it's state/data with a scene,   
                and not individual objects/tags within that scene.   
                  
                TO DO:   
                ------------   
                  
                [] Simple test with preset text   
                  
                """   
                  
                #IMPORTS   
                import c4d   
                from c4d import gui, plugins   
                  
                #LOGGING   
                import logging   
                logging.basicConfig()   
                logger = logging.getLogger()   
                logger.setLevel(logging.INFO) #Logging levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL   
                  
                #GLOBALS   
                PLUGIN_ID = 1032093   
                  
                #DIALOG IDS   
                DLG_EDIT_TEXT = 10001   
                DLG_SAVE = 10002   
                DLG_LOAD = 10003   
                  
                #CONTAINER IDs   
                BC_ID_NAME = 0   
                  
                  
                #CLASS DEFINITIONS   
                class DocDataDialog(gui.GeDialog) :   
                    """A two-button dialog that stores data in the document's settings _hook."""   
                  
                    def __init__(self) :   
                        """Initializes variables needed by funtions in this class."""   
                  
                        logger.debug("DocDataDialog()")   
                  
                        self._last_doc = None #Last document seen by the dialog, can be used to detect new documents   
                        self._hook = None #The most-recent document settings object, so that we have easy access to it   
                  
                        self._update_dialog = True #A flag that can be set which dictates whether the dialog should update on refresh   
                  
                        self.Refresh()   
                  
                    def CreateLayout(self) :   
                        """Adds buttons to your layout."""   
                  
                        logger.debug("DocDataDialog.CreateLayout()")   
                  
                        self.AddStaticText(DLG_EDIT_TEXT, c4d.BFH_SCALEFIT)   
                        self.AddButton(DLG_SAVE, c4d.BFH_CENTER, 0, 10, name="Save Text to Container")   
                        self.AddButton(DLG_LOAD, c4d.BFH_CENTER, 0, 10, name="Load Text from Container")   
                  
                        #Fill the dialog with data   
                        self.Refresh()   
                  
                        return True   
                  
                    def InitValues(self) :   
                        """No data needs to be initialized."""   
                  
                        logger.debug("DocDataDialog.InitValues()")   
                        return True   
                  
                    def Command(self, id, msg) :   
                        """Respond to user button presses."""   
                  
                        logger.debug("DocDataDialog.Command(id=%s, msg=%s)" % (id, msg))   
                  
                        #Store value in a container when this button is pressed   
                        if id == DLG_SAVE:   
                            logger.debug("DLG_SAVE")   
                  
                            self._last_doc.StartUndo()   
                  
                            #Store the data from the dialog   
                            self._last_doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, self._hook)   
                            plugin_bc = c4d.BaseContainer()   
                            plugin_bc[BC_ID_NAME] = "Saved Data"   
                            self._hook.GetDataInstance().SetContainer(PLUGIN_ID, plugin_bc)   
                  
                            self._last_doc.EndUndo()   
                  
                            self._update_dialog = False #Prevent loading data just in case the user presses undo   
                            c4d.EventAdd()   
                  
                        #Read the container when this button is pressed   
                        if id == DLG_LOAD:   
                            logger.debug("DLG_LOAD")   
                            self._update_dialog = True   
                            self.Refresh()   
                  
                        return True   
                  
                    def Refresh(self) :   
                        """Reload the data from the BaseSettings _hook. Call when there's a new scene or a significant change."""   
                  
                        logger.debug("DocDataDialog.Refresh()")   
                  
                        self._last_doc = c4d.documents.GetActiveDocument()   
                        self._hook = self._last_doc.FindSceneHook(c4d.ID_BS_HOOK)   
                  
                        hook_bc = self._hook.GetDataInstance()   
                        plugin_bc = hook_bc.GetContainer(PLUGIN_ID)   
                  
                        if self._update_dialog:   
                            saved_string = plugin_bc[BC_ID_NAME]   
                            if saved_string is None:   
                               saved_string = ""   
                  
                            #Take the stored data and show it in the dialog   
                            self.SetString(DLG_EDIT_TEXT, saved_string)   
                  
                    def CoreMessage(self, kind, bc) :   
                        """Respond to messages from Cinema 4D"""   
                  
                        logger.debug("DocDataDialog.CoreMessage(kind=%s, bc=%s)" % (kind, bc))   
                  
                        #Something has changed in the document   
                        if kind == c4d.EVMSG_DOCUMENTRECALCULATED:   
                            logger.debug("c4d.EVMSG_DOCUMENTRECALCULATED")   
                  
                            #Refresh the dialog to account for whatever these changes may be.   
                            self.Refresh()   
                  
                        return True   
                  
                  
                #REGISTRATION   
                class DocDataCommand(plugins.CommandData) :   
                    """A command plugin which calls up our dialog."""   
                  
                    dialog = None   
                  
                    def Execute(self, doc) :   
                        """When the user calls this command, open an instance of our dialog."""   
                  
                        logger.debug("DocDataCommand.Execute(_last_doc=%s)" % (doc,))   
                  
                        #Create the dialog   
                        if self.dialog is None:   
                            self.dialog = DocDataDialog()   
                        return self.dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=PLUGIN_ID,   
                                                defaultw=200, defaulth=150, xpos=-1, ypos=-1)   
                  
                #Register our plugin   
                if __name__ == "__main__":   
                    plugins.RegisterCommandPlugin(PLUGIN_ID, "UndoTest", 0, None, "", DocDataCommand())   
                
                1 Reply Last reply Reply Quote 0
                • H
                  Helper
                  last edited by

                  On 08/05/2014 at 17:00, xxxxxxxx wrote:

                  Thanks Donovan.
                  It's a bit confusing. But I think I see what is happening.

                  You're still using a button to make the undo happen. In this case the DLG_LOAD button.
                  You just go through an extra step and make it happen when the _update_dialog variable is set to True. But it's still only occurring when the user physically changes a gizmo.

                  If you click the DLG_SAVE button. The dialog does not show "Saved Data" in it.
                  But the container "plugin_bc" was created and is there in the SceenHook.
                  And if you undo the document. It does not undo that container.

                  In short.
                  What you're doing is undoing the dialog...but not undoing the the document (the SceenHook).
                  It's a bit confusing to follow. But I think I understand what's going on.

                  -ScottA

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