Hello @wuzelwazel,
thank you for reaching out to us. Unfortunately, it is very unlikely that we will ever implement something like this, as it goes into the direct opposite direction of where we want to go - parity between C++ and Python. But even if this restriction would not be there, I would be hesitant to do this, because one could end up with code like this:
with MyUndoContext(*args):
#some code ...
if some_condition:
doc.EndUndo()
with MyUndoContext(*args):
# some code
doc.StartUndo()
I.e. when you have an undo wrapped in an undo, you would have an EndUndo
which refers to the enclosing context, then a nested context and then reopen the previously closed undo context. Which does not read very well and would in the case of Cinema 4D also not work. There is also a problem with providing the undo document, which is sort of undefined for UNDOTYPE_NEW
, which leads to a bit weird argument syntax when implementing this.
But implementing it yourself is not that hard. Below you can find an example implementation which can be used like this:
with UndoContext(node, c4d.UNDOTYPE_CHANGE):
node[c4d.ID_BASEOBJECT_REL_POSITION] = c4d.Vector(100, 0, 0)
Cheers,
Ferdinand
"""Example for implementing a context manger for Undo operations.
As discussed in:
https://plugincafe.maxon.net/topic/13358
"""
import c4d
# The relevant Undo flags.
UNDO_TYPES = (
c4d.UNDOTYPE_NONE,
c4d.UNDOTYPE_CHANGE,
c4d.UNDOTYPE_CHANGE_NOCHILDREN,
c4d.UNDOTYPE_CHANGE,
c4d.UNDOTYPE_CHANGE_SMALL,
c4d.UNDOTYPE_CHANGE_SELECTION,
c4d.UNDOTYPE_NEW,
c4d.UNDOTYPE_DELETE,
c4d.UNDOTYPE_BITS,
c4d.UNDOTYPE_HIERARCHY_PSR
)
class UndoContext():
"""A class based context manager for Cinema 4D Undo operations.
This could also be done with decorator gymnastics and I would actually
prefer that approach due to its greater flexibility. But I think this is
the currently the recommended way to implement context mangers for
Python 3. So, class approach it is :)
"""
def __init__(self, node, flag, doc=None):
"""Context Initializator.
Args:
node (c4d.BaseList2D): The node to add an undo for.
flag (int): The undo flag.
doc (None | c4d.documents.BaseDocument, optional): The document
to add the undo to. Has only to be provided for UNDOTYPE_NEW,
since the document there is not defined yet for a node. Defaults
to None.
Raises:
TypeError: For invalid argument types.
ValueError: For invalid flag values/types.
"""
# Sort out some invalid arguments.
if flag not in UNDO_TYPES:
raise ValueError(f"Unexpected flag symbol: {flag}")
if not isinstance(node, c4d.BaseList2D):
raise TypeError(f"Unexpected node type: {type(node)}")
doc = doc or node.GetDocument()
if not isinstance(doc, c4d.documents.BaseDocument):
raise TypeError(f"Unexpected document type: {type(doc)}")
# Some private attributes.
self._node = node
self._doc = doc
self._flag = flag
def __enter__(self):
"""Called when the context is being entered.
"""
self._doc.StartUndo()
# All Undo operations except UNDOTYPE_NEW have to be invoked before
# the changes to be applied.
if self._flag != c4d.UNDOTYPE_NEW:
self._doc.AddUndo(self._flag, self._node)
def __exit__(self, type, value, traceback):
"""Called when the context is being exited.
"""
if self._flag == c4d.UNDOTYPE_NEW:
self._doc.AddUndo(self._flag, self._node)
self._doc.EndUndo()
def main():
"""Example usage.
"""
# When there is an active object, modify its position wrapped in an undo.
if op is not None:
with UndoContext(op, c4d.UNDOTYPE_CHANGE):
op[c4d.ID_BASEOBJECT_REL_POSITION] = c4d.Vector(100, 0, 0)
# Add a new cube object wrapped in an undo.
myCube = c4d.BaseList2D(c4d.Ocube)
with UndoContext(myCube, c4d.UNDOTYPE_NEW, doc):
myCube[c4d.PRIM_CUBE_DOFILLET] = True
myCube[c4d.PRIM_CUBE_FRAD] = 25.
doc.InsertObject(myCube)
# Push update notification to Cinema 4D.
c4d.EventAdd()
if __name__ == '__main__':
main()