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