How To Add Undo For Description Added Dynamically
-
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()
, andEndUndo()
) intoMessage()
function but it doesn't seem to work.
My goal is it works like "IK-Spline" does. (see below)"Current Status"
"My Goal"
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 -
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.) -
@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.
-
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 -------------------- }
-
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. -
@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! -
@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.
-
@Cairyn Thank you so much for helping me again!