Dynamically adding parameters in tag plugin - description is not refreshing
-
Hi, in my object plugin it worked fine but in my tag plugin the description cannot be updated after clicking a button which in this example should add a new button.
so when I click the button it adds a new id to the member variable self.id_list, and then I iter through this list in my GetDDescription() method
It just shows the added Button if I click on the Object which holds the tag and select again the tag.
Do I have to send a message? I read the GitHubExample but it is a example with an ObjectPlugin.import c4d from c4d import bitmaps, gui, plugins # Just a test ID PLUGIN_ID = 1110101 PY_ADD_TRACK = 10000 #The Plugin Class class Audioworkstation(plugins.TagData): def __init__(self): self.id_list = [] def Init(self, node): return True def GetDDescription(self, op, description, flags): if not description.LoadDescription(op.GetType()): return False singleID = description.GetSingleDescID() # id_counter = 1 for desc_id in reversed(self.id_list): measure_object_hide = c4d.DescID(c4d.DescLevel(desc_id + 2, c4d.DTYPE_BUTTON, op.GetType())) if not singleID or measure_object_hide.IsPartOf(singleID)[0]: bc_measure_object_hide = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BUTTON) bc_measure_object_hide[c4d.DESC_NAME] = "DELETE" bc_measure_object_hide[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_BUTTON # id_counter += 1 if not description.SetParameter(measure_object_hide, bc_measure_object_hide, c4d.DescID(c4d.DescLevel( c4d.ID_TAGPROPERTIES))): return False return True, flags | c4d.DESCFLAGS_DESC_LOADED 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 == PY_ADD_TRACK: last_id = None if self.id_list: last_id = self.id_list[-1] new_id = last_id + 100 self.id_list.append(new_id) else: new_id = 10100 self.id_list.append(new_id) node.Message(c4d.MSG_CHANGE) return True # main if __name__ == "__main__": bmp = bitmaps.BaseBitmap() dir, file = os.path.split(__file__) fn = os.path.join(dir, "res", "icon.tif") bmp.InitWith(fn) c4d.plugins.RegisterTagPlugin(id=PLUGIN_ID, str="C4D-Audioworkstation", info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE, description="audioworkstation", g=Audioworkstation, icon=bmp)
edit by @ferdinand:
@ThomasB said:
I have also problems to scale the button so that it fits the attribute manager:
so this doesn't work in the GetDDescription Example above:
bc_measure_object_hide[c4d.DESC_SCALEH] = True bc_measure_object_hide[c4d.DESC_FITH] = True
-
Hello @ThomasB,
Thank you for reaching to us. While you provided your code and a clear and brief problem description - thank you for that - your example was lacking the resources. Please provide your resources (the
res
folder) or ideally a full plugin in future cases, especially when like here the description/resource is the subject of the posting.Please also follow our rules of "consolidate your postings" and "one subject per topic". I have consolidated the posting here for you but must ask you to open another topic for your second question. For details please refer to our Support Procedures.
Find my full code at the end of the posting.
About your Issue
TLDR: You must flag yourself dirty.
Your code does everything correctly apart from some smaller issues and it took myself quite some time to figure out what is going wrong here. Due to that I rewrote your code to rule out that I overlooked something in your code and to streamline things. I ended up with this for the core methods:
def __init__(self): """ """ self.buttonIds: list[int] = [2000] def GetDDescription(self, op, description, flags): """ """ if not description.LoadDescription(Audioworkstation.PLUGIN_ID): return False target: c4d.DescID | None = description.GetSingleDescID() buttonIds: list[c4d.DescID] = [ c4d.DescID(c4d.DescLevel(did, c4d.DTYPE_BUTTON, Audioworkstation.PLUGIN_ID)) for did in self.buttonIds ] for eid in buttonIds: # The eid isPartOf target is an echo of what you did in your code which does not make too # much sense. I copied it here as it also does not hurt. if target and not eid.IsPartOf(target)[0]: continue buttonDesc: c4d.BaseContainer = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BUTTON) buttonDesc[c4d.DESC_NAME] = str(eid) buttonDesc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_BUTTON if not description.SetParameter(eid, buttonDesc, c4d.DescID(c4d.ID_TAGPROPERTIES)): return False return True, flags | c4d.DESCFLAGS_DESC_LOADED def Message(self, node, mtype, mdata): """ """ if (mtype == c4d.MSG_DESCRIPTION_COMMAND and isinstance(mdata, dict)): descId: c4d.DescID = mxutils.CheckType(mdata.get("id", None)) if descId.GetDepth() < 1 or descId[0].id != Audioworkstation.ID_AUDIOWORKSTATION_ADD: return True self.buttonIds.append(self.buttonIds[-1] + 100) return True return True
I first could not wrap my head around why this is not working, as we have examples like
dynamic_parameters_object_r18.py
and this is still working fine. But then I realized that all these examples use elements like sliders or integer fields to add or remove elements, not buttons. And these elements are handled differently when it comes to user interaction. And sure enough, when I added aLONG
field to the description and its handling to the code, it works. I treat theLONG
fieldID_AUDIOWORKSTATION_ITEMS
like a button, i.e., I ignore if its goes up or down, or what its values is. The purpose is here to switch fromMSG_DESCRIPTION_COMMAND
toMSG_DESCRIPTION_POSTSETPARAMETER
.if (mtype == c4d.MSG_DESCRIPTION_COMMAND and isinstance(mdata, dict)): descId: c4d.DescID = mxutils.CheckType(mdata.get("id", None)) if descId.GetDepth() < 1 or descId[0].id != Audioworkstation.ID_AUDIOWORKSTATION_ADD: return True self.buttonIds.append(self.buttonIds[-1] + 100) print ("Command: Add") return True if (mtype == c4d.MSG_DESCRIPTION_POSTSETPARAMETER and isinstance(mdata, dict)): descId: c4d.DescID = mxutils.CheckType(mdata.get("descid", None)) if descId.GetDepth() < 1 or descId[0].id != Audioworkstation.ID_AUDIOWORKSTATION_ITEMS: return True self.buttonIds.append(self.buttonIds[-1] + 100) print ("Command: Items")
And this makes sense to a certain degree in the Cinema logic that that the description is not fully beaing updated after
MSG_DESCRIPTION_COMMAND
has been emitted and only whenMSG_DESCRIPTION_POSTSETPARAMETER
is emitted. But we clearly have scene elements which have buttons in their UI to add and remove other UI elements, the constraint tag for example. So, I looked there and this clarified things:case MSG_DESCRIPTION_COMMAND: if (doc) { DescriptionCommand* dc = (DescriptionCommand*)msgdata; BaseContainer* bc = static_cast<BaseList2D*>(node)->GetDataInstance(); // All the constraint Add Remove etc. button code only runs when a undo has been added // to the object, as the buttons not only increment some internal data but also modify // the data container of the node, i.e., entail a MSG_DESCRIPTION_POSTSETPARAMETER. if (msgdata && bc && doc->AddUndo(UNDOTYPE::CHANGE, node) && dc) // FIX[54204] { // ... } }
So,
MSG_DESCRIPTION_COMMAND
on its own is insufficient to modify the UI, as it will not entail a full description update. We must either modify our own data, or cheat a bit, and just flag ourselves as dirty although nothing has changed (see end of posting for code). And if we do that, our button works:A beautiful case of Cinema 4D API logic
Cheers,
FerdinandFile
Code
import c4d from c4d import plugins import mxutils class Audioworkstation(plugins.TagData): """Your plugin class. """ PLUGIN_ID: int = 7654321 def Execute(self, tag, doc, op, bt, priority, flags): return c4d.EXECUTIONRESULT_OK def __init__(self) -> None: """ """ self.buttonIds: list[int] = [2000] def Init(self, node, isCloneInit) -> bool: """Init our parameters. """ self.InitAttr(node, int, c4d.ID_AUDIOWORKSTATION_ITEMS) if not isCloneInit: node[c4d.ID_AUDIOWORKSTATION_ITEMS] = 0 return True def GetDDescription(self, op: c4d.BaseTag, description: c4d.Description, flags: int) -> tuple[bool, int]: """More or less your code, I just streamlined things a bit. """ if not description.LoadDescription(Audioworkstation.PLUGIN_ID): return False target: c4d.DescID | None = description.GetSingleDescID() buttonIds: list[c4d.DescID] = [ c4d.DescID(c4d.DescLevel(did, c4d.DTYPE_BUTTON, Audioworkstation.PLUGIN_ID)) for did in self.buttonIds ] for eid in buttonIds: # The eid.IsPartOf(target) is an echo of what you did in your code which does not make too # much sense. I copied it here as it also does not hurt. eid.IsPartOf(target) basically # says "if the made up id #eid of the to be added element is part of the description # element Cinema 4D is currently polling the description for, then do this". Which does # not make too much sense. It is the #target or the #singleDescId condition in your code # which makes this work. if target and not eid.IsPartOf(target)[0]: continue buttonDesc: c4d.BaseContainer = c4d.GetCustomDataTypeDefault(c4d.DTYPE_BUTTON) buttonDesc[c4d.DESC_NAME] = str(eid) buttonDesc[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_BUTTON if not description.SetParameter(eid, buttonDesc, c4d.DescID(c4d.ID_TAGPROPERTIES)): return False return True, flags | c4d.DESCFLAGS_DESC_LOADED def Message(self, node: c4d.BaseTag, mtype: int, mdata: any) -> bool: """ """ # Our Add button is being pressed, we want to add an item to the UI. if (mtype == c4d.MSG_DESCRIPTION_COMMAND and isinstance(mdata, dict)): descId: c4d.DescID = mxutils.CheckType(mdata.get("id", None)) if descId.GetDepth() < 1 or descId[0].id != c4d.ID_AUDIOWORKSTATION_ADD: return True doc: c4d.documents.BaseDocument = node.GetDocument() if not doc: return True # We add a new ID to our button IDs. self.buttonIds.append(self.buttonIds[-1] + 100) # Now we have two options, either we do what the constraint tag does, and now modify # the data container of the node as a result of the button press and with that trigger # the MSG_DESCRIPTION_POSTSETPARAMETER message. # doc.StartUndo() # doc.AddUndo(c4d.UNDOTYPE_CHANGE, node) # node[c4d.ID_AUDIOWORKSTATION_ITEMS] = node[c4d.ID_AUDIOWORKSTATION_ITEMS] + 1 # doc.EndUndo() # Or we just flag ourselves as data dirty, i.e., we pretend that our data has changed, # this is enough to sweet talk Cinema into a description update. node.SetDirty(c4d.DIRTYFLAGS_DATA) # PS: I also tried c4d.SendCoreMessage(c4d.COREMSG_CINEMA_FORCE_AM_UPDATE, ...) but # that does not work in this case, we really have to fake our own dirtiness. return True return True # main if __name__ == "__main__": c4d.plugins.RegisterTagPlugin(id=Audioworkstation.PLUGIN_ID, str="C4D-Audioworkstation", info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE, description="audioworkstation", g=Audioworkstation, icon=None)
-
@ferdinand
Thanks a lot Ferdinand for your time and effort. It is always admirable how carefully and thoroughly you answer many questions.
At first it was often difficult to understand and follow your code examples...now it is a little easier.
Thanks for that.I found out that the following method also does the job:
c4d.SendCoreMessage(c4d.COREMSG_CINEMA, c4d.BaseContainer(c4d.COREMSG_CINEMA_FORCE_AM_UPDATE), 0)
Sorry for the second question about why this
buttonDesc[c4d.DESC_FITH] = True buttonDesc[c4d.DESC_SCALEH] = True
in the GetDDescription method are not working. I thought this is a follow-up question.
I will open another topic for that.