Undo BaseDocument Container Changes?



  • 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



  • 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. :D
    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



  • 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



  • 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



  • 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



  • 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())   
    


  • 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


Log in to reply