Treeview does not refresh
-
Hi guys,
I'm sorry to bother you again with questions regarding Treeviews. But I noticed that the Treeview is not refreshing when wrapping Cinemas native objects into a custom class for the Treeview to display.
Here's the code I'm using. It's a simple Treeview example made by Donovan Keith.
Just create some materials and execute the script. The Treeview should display those materials. You will however notice that it is not updating when you for example create new materials or delete existing ones.
One could argue why not simply use Cinemas native Baselist2D objects directly. The problem is that there seems to be an issue when it comes to selection states when using the native objects in the Treeview. As discussed here. With a custom class, selecting, adding, subtracting even shift-selecting works as expected. While using Baselist2D objects it does not.
Cheers,
Sebastian"""ObjectBrowser Based on: [URL-REMOVED]""" # ====== IMPORTS ====== # import c4d # ====== GLOBALS ====== # debug = True PLUGIN_ID = 1037588 PLUGIN_NAME = "Object Browser" PLUGIN_HELP = "A simple treeview example" class ListItem(object): """A wrapper class for c4d.BaseList2D.""" def __init__(self, obj): self.obj = obj def GetName(self): return self.obj.GetName() def IsSelected(self): return self.obj.GetBit(c4d.BIT_ACTIVE) def Select(self): self.obj.SetBit(c4d.BIT_ACTIVE) def Deselect(self): self.obj.DelBit(c4d.BIT_ACTIVE) def IsOpened(self): return self.obj.GetBit(c4d.BIT_OFOLD) # ====== TREEVIEW ====== # class ObjectTree(c4d.gui.TreeViewFunctions): """Data structure for a TreeView of Materials & Shaders.""" def __init__(self, dlg): self.items = [] self.LoadObjects() def LoadObjects(self): doc = c4d.documents.GetActiveDocument() if not doc: return obj = doc.GetFirstMaterial() while obj: wrapped_obj = ListItem(obj) self.items.append(wrapped_obj) obj = obj.GetNext() def GetFirst(self, root, userdata): """Returns the first Material in the document.""" if self.items: return self.items[0] def GetDown(self, root, userdata, obj): """Get the next shader in the list.""" return None def GetNext(self, root, userdata, obj): """Get the next material/shader in the list.""" if obj in self.items: obj_index = self.items.index(obj) next_index = obj_index + 1 if next_index < len(self.items): return self.items[next_index] def GetPred(self, root, userdata, obj): if obj in self.items: obj_index = self.items.index(obj) prev_index = obj_index - 1 if prev_index > 0: return self.items[prev_index] def GetName(self, root, userdata, obj): """Returns the name of obj.""" if not obj: return return obj.GetName() def IsOpened(self, root, userdata, obj): """Returns True if obj is unfolded.""" return obj.IsOpened() def IsSelected(self, root, userdata, obj): """Returns True if obj is selected.""" return obj.IsSelected() def Select(self, root, userdata, obj, mode): """Selects `obj` based on `mode`.""" if mode == c4d.SELECTION_NEW: for item in self.items: item.Deselect() if item == obj: item.Select() elif mode == c4d.SELECTION_ADD: obj.Select() elif mode == c4d.SELECTION_SUB: obj.Deselect() def Open(self, root, userdata, obj, onoff): """Folds or unfolds obj based on onoff.""" if not obj: return if onoff: obj.SetBit(c4d.BIT_OFOLD) else: obj.DelBit(c4d.BIT_OFOLD) c4d.EventAdd() # ====== DIALOG ====== # class ObjectBrowser(c4d.gui.GeDialog): """Dialog that contains a list of Materials & Shaders in the active document.""" _tree_gui = None def CreateLayout(self): """Build the overall dialog layout.""" self.SetTitle(PLUGIN_NAME) # Build the ShaderTree GUI Element tree_gui_settings = c4d.BaseContainer() tree_gui_settings.SetLong(c4d.TREEVIEW_BORDER, c4d.BORDER_THIN_IN) tree_gui_settings.SetBool(c4d.TREEVIEW_HAS_HEADER, True) tree_gui_settings.SetBool(c4d.TREEVIEW_HIDE_LINES, False) tree_gui_settings.SetBool(c4d.TREEVIEW_MOVE_COLUMN, True) tree_gui_settings.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True) tree_gui_settings.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True) # Don't allow Columns to be re-ordered tree_gui_settings.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True) # Alternate Light/Dark Gray BG tree_gui_settings.SetBool(c4d.TREEVIEW_CURSORKEYS, True) # Process Up/Down Arrow Keys self._tree_gui = self.AddCustomGui( 0, c4d.CUSTOMGUI_TREEVIEW, "", c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 300, 300, tree_gui_settings ) return True def InitValues(self): """Set initial values when dialog is first opened.""" tree_data = ObjectTree(self) self._tree_gui.SetRoot(None, tree_data, None) return True def CoreMessage(self, id, msg): if id == c4d.EVMSG_CHANGE: print("c4d.EVMSG_CHANGE") self._tree_gui.Refresh() return True # ====== COMMAND ====== # class ObjectBrowserCommand(c4d.plugins.CommandData): """Command that opens a ObjectTree dialog.""" dlg = None def Execute(self, doc): if self.dlg is None: self.dlg = ObjectBrowser() return self.dlg.Open( dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=PLUGIN_ID, xpos=-1, ypos=-1, defaultw=300, defaulth=500 ) def GetState(self, doc): return c4d.CMD_ENABLED def RestoreLayout(self, sec_ref): if self.dlg is None: self.dlg = ObjectBrowser() return self.dlg.Restore(PLUGIN_ID, secret=sec_ref) def main(): """Register the plugin with Cinema 4D.""" global dialog dialog = ObjectBrowser() dialog.Open( c4d.DLG_TYPE_ASYNC, PLUGIN_ID, defaulth=300, defaultw=300 ) if __name__ == "__main__": main()
[URL-REMOVED] @maxon: This section contained a non-resolving link which has been removed.
-
Hello @herrmay,
Thank you for reaching out to us and no worries, we are here to answer your questions. That your material browser does not update is not too surprising, because you never update the data.
The classic API does not have a too granular event system and one is often forced to react to the very broad core message
EVMSG_CHANGE
which just conveys that something has changed in a scene, e.g., a material has been add, removed, renamed, etc. - or that the polygon object in the top left corner of the active camera changed its opinion about what is its favorite vertex So,EVMSG_CHANGE
is being fired a lot and reacting to it directly can produce unwanted overhead or, when you do not care about overhead, can lead to unwanted behaviours when a system updates when it is not meant to update.You implement a dialog here which receives such core messages via
GeDialog.CoreMessage
, you even use some code which has 90% of the work done. You just have to update you actual data when the message is being fired.def CoreMessage(self, id, msg): if id == c4d.EVMSG_CHANGE: # Reinitialize the object tree so that it reflects the new scene state. We could write # custom code or be lazy like me and reuse InitValues() :) . In practice you might want # to be a bit more selective about when you reinitialize your ObjectTree, as doing # so will flush the selection state etc. You could for example by caching a list of # UUIDs of all materials and on every EVMSG_CHANGE check if something about this list # changed, i.e., if a material has been added or removed. If you would also track # the data container dirty count of each material in that list, you would have a pretty # water tight "on_material_data_change" event. # # The alternative would be to fashion your ObjectTree so that a running instance can # be updated with scene data. In both cases you will need a solid understanding of # node UUIDs to identify materials as Python's id() will not work due to nodes being # reallocated. Here we talked about them in the context of materials: # https://developers.maxon.net/forum/topic/14266/ # You can find more UUID posting made by me under: # https://developers.maxon.net/forum/search?term=UUID&in=titlesposts&by[]=ferdinand self.InitValues() self._tree_gui.Refresh() return True
As lined out in the comment, there are more complex approaches to this. It might also be a good idea to read the Message System Manual
Cheers,
FerdinandResult
-
Hello @ferdinand,
first of all thank you for your endless effort and the detailed explanations and solutions you always come up with.
I heard what you said regarding
c4d.EVMSG_CHANGE
being a possible but rather broad notification about changes in a scene. In theory I understand. In practice - well that's another story.The problem with calling
self.InitValues()
on every change in a document is that I can not select items in the Treeview any longer because that in itself is a change which callsc4d.EVMSG_CHANGE
. Which in turn invokesself.InitValues()
again. I guess this is what you meant when you were talking about screwing up selection states.I actually read the posts you mentioned about
c4d.MAXON_CREATOR_ID
prior to this thread and also about your fantastic example about "permanently" storing a collection of materials. I stole that part and implemented it in the code below to distinguish between changes.My idea was to compare a stored collection with a newly created one. If they differ in length materials must have been created or removed. In addition to that I compare the dirty states of the collections. If they differ something in the data of a material must have changed which means a user changed some values. In both cases I update the Treeview. It works but to be honest neither do I know if this a viable solution nor if it is "the" right way to do it.
The problem is that every time the Treeview is updated the folding of items isn't respected. It makes sense because
self.InitValues()
calls the code with the initial state of the node. Which in my case isTrue
. I struggle to find a solution as how to store the new state of the folding so that a potential update of the Treeview respects it.@ferdinand I don't expect you or anyone to write complete solutions for me, but I'm more than thankful if someone can tell if my idea of comparing two collections is a good or maybe "the" way to go. If someone can also shed some light on the part folding part that would be awesome.
Thanks for reading this rather long babbling of mine.
Cheers,
Sebastian#More information https://developers.maxon.net/forum/topic/10654#56287 import c4d import weakref # Be sure to use a unique ID obtained from [URL-REMOVED] PLUGIN_ID = 1000010 # TEST ID ONLY # TreeView Column IDs. ID_CHECKBOX = 1 ID_TYPE = 2 ID_NAME = 3 class MaterialCollection(object): """Handles a collection of materials from a singular document for long-term referencing. Will reestablish material references when possible and also handles a document reference. """ def __init__(self, doc, collection): """Initializes the collection with a list of materials. """ if not isinstance(collection, list): raise TypeError("{collection = }".format(collection=collection)) if not isinstance(doc, c4d.documents.BaseDocument): raise TypeError("{doc = }".format(doc=doc)) if not doc.IsAlive(): raise RuntimeError("{doc = } is not alive.".format(doc=doc)) # The list of materials, the list of markers for them, their associated document, and its # marker. self._materialCollection = [] self._materialMarkerCollection = [] self._materialDirtyCount = [] self._doc = doc self._docMarker = MaterialCollection.GetUUID(doc) # Validate all materials and get their markers, we must do that before the reference is dead. for material in collection: if not isinstance(material, c4d.BaseMaterial) or not material.IsAlive(): raise RuntimeError("{material} is not alive or not a BaseMaterial.".format(material=material)) if material.GetDocument() is None: raise RuntimeError("Dangling material cannot be managed.") if material.GetDocument() != doc: raise RuntimeError("Material is not part of passed reference document.") # Append the material and its UUID marker to the internal lists. self._materialCollection.append(material) self._materialMarkerCollection.append(MaterialCollection.GetUUID(material)) self._materialDirtyCount.append(material.GetDirty(c4d.DIRTYFLAGS_DATA)) def __repr__(self): return "{}({})".format(self.__class__.__name__, len(self)) def __len__(self): """Returns the number of managed materials. """ return len(self._materialCollection) def __getitem__(self, index): """Returns the material at #index. """ if len(self._materialCollection) - 1 < index: raise IndexError("The index '{index}' is out of bounds.".format(index=index)) # The item is still alive, we can return it directly. item = self._materialCollection[index] if item.IsAlive(): return item # --- The item is not alive anymore, try to find its new instance. ------------------------- # Get the marker identifying the material at #index. materialMarker = self._materialMarkerCollection[index] # Iterate over all materials in #_doc to find one with a matching marker. We now call here # the property #Document instead of _doc, to also allow for reestablishing the document # reference. for material in self.Document.GetMaterials(): otherMarker = MaterialCollection.GetUUID(material) # We found a match, update the internal data, and return the material. if materialMarker == otherMarker: self._materialCollection[index] = material return material # There is no material with this marker anymore. raise RuntimeError( "The material at {index} is not valid anymore and cannot be re-established.".format(inde=index)) def __iter__(self): """Iterates over all materials in the collection. """ for index in range(len(self._materialCollection)): yield self[index] @property def Document(self): """Returns the managed document reference. Will try to reestablish a document reference within the open documents when necessary. """ # Return the still valid reference. if self._doc.IsAlive(): return self._doc # Search all open documents for one with a UUID that matches self._docMarker. doc = c4d.documents.GetFirstDocument() while doc: docMarker = MaterialCollection.GetUUID(doc) if docMarker == self._docMarker: self._doc = doc return doc doc = doc.GetNext() # We could also search and load documents from #c4d.documents.GetRecentDocumentsList() here # but I did not do that, as it seems unlikely that you want to do that. raise RuntimeError( "A reference to the document UUID '{self._docMarker}' cannot be reestablished. " "The document has probably been closed.".format(self._docMarker)) @property def Materials(self): """Returns an immutable collection of the internally managed materials. Might contain dangling material references, i.e., "dead" materials. """ return tuple(self._materialCollection) @property def Markers(self): """Returns an immutable collection of the internally managed material markers. """ return tuple(self._materialMarkerCollection) @property def Dirty(self): """Returns an immutable collection of the internally managed material dirty state. """ return tuple(self._materialDirtyCount) @staticmethod def GetUUID(atom): """Retrieves the UUID of an atom as a bytes object. Args: item (c4d.C4DAtom): The atom to get the UUID hash for. Raises: RuntimeError: On type assertion failure. Returns: bytes: The UUID of #atom. """ if not isinstance(atom, c4d.C4DAtom): raise TypeError(atom) # Get the MAXON_CREATOR_ID marker uniquely identifying the atom. uuid = atom.FindUniqueID(c4d.MAXON_CREATOR_ID) #if not isinstance(uuid, memoryview): # raise RuntimeError("Illegal non-marked atom: {atom}".format(atom=atom)) return bytes(uuid) def iter_shaders(node): """Yields all descendants of ``node`` in a truly iterative fashion. The passed node itself is yielded as the first node and the node graph is being traversed in depth first fashion. This will not fail even on the most complex scenes due to truly hierarchical iteration. The lookup table to do this, is here solved with a dictionary which yields favorable look-up times in especially larger scenes but results in a more convoluted code. The look-up could also be solved with a list and then searching in the form ``if node in lookupTable`` in it, resulting in cleaner code but worse runtime metrics due to the difference in lookup times between list and dict collections. """ if not node: return # The lookup dictionary and a terminal node which is required due to the # fact that this is truly iterative, and we otherwise would leak into the # ancestors and siblings of the input node. The terminal node could be # set to a different node, for example ``node.GetUp()`` to also include # siblings of the passed in node. visisted = {} terminator = node while node: #if isinstance(node, c4d.Material) and not node.GetFirstShader(): # break if isinstance(node, c4d.Material) and node.GetFirstShader(): node = node.GetFirstShader() # C4DAtom is not natively hashable, i.e., cannot be stored as a key # in a dict, so we have to hash them by their unique id. node_uuid = node.FindUniqueID(c4d.MAXON_CREATOR_ID) if not node_uuid: raise RuntimeError("Could not retrieve UUID for {}.".format(node)) # Yield the node when it has not been encountered before. if not visisted.get(bytes(node_uuid)): yield node visisted[bytes(node_uuid)] = True # Attempt to get the first child of the node and hash it. child = node.GetDown() if child: child_uuid = child.FindUniqueID(c4d.MAXON_CREATOR_ID) if not child_uuid: raise RuntimeError("Could not retrieve UUID for {}.".format(child)) # Walk the graph in a depth first fashion. if child and not visisted.get(bytes(child_uuid)): node = child elif node == terminator: break elif node.GetNext(): node = node.GetNext() else: node = node.GetUp() def walk_shadertree(mat): for shader in iter_shaders(mat): parent = shader.GetUp() if shader.GetUp() else mat pred = shader.GetPred() if shader.GetPred() else None nxt = shader.GetNext() if shader.GetNext() else None children = shader.GetChildren() if shader.GetDown() else [] yield { "node": shader.GetName(), "parent": parent.GetName() if parent else parent, "pred": pred.GetName() if pred else pred, "nxt": nxt.GetName() if nxt else nxt, "children": [child.GetName() for child in children] } def NodeIterator(lst): for parent in lst: yield parent for child in NodeIterator(parent.GetChildren()): yield child class Node(object): """Class which represent a an item in our Tree.""" def __init__(self, obj): self.obj = obj self.children = [] self._selected = False self._open = True self._parent = None def __repr__(self): return str(self) def __str__(self): return self.obj.GetName() @property def IsSelected(self): return self._selected def Select(self): self._selected = True self.obj.SetBit(c4d.BIT_ACTIVE) def Deselect(self): self._selected = False self.obj.DelBit(c4d.BIT_ACTIVE) @property def IsOpen(self): return self._open def Open(self): self._open = True self.obj.DelBit(c4d.BIT_OFOLD) def Close(self): self._open = False self.obj.SetBit(c4d.BIT_OFOLD) def AddChild(self, obj): obj._parent = weakref.ref(self) self.children.append(obj) def GetChildren(self): return self.children def GetParent(self): if self._parent: return self._parent() return None def GetName(self): return self.obj.GetName() def GetTypeName(self): return self.obj.GetTypeName() def SetName(self, name): self.obj.SetName(name) def recurse_shader(shader, obj): # Not really happy with this algorithm. # It works but the recursion is bothering me. if shader.GetDown(): shader = shader.GetDown() while shader: child = Node(shader) obj.AddChild(child) if shader.GetDown(): recurse_shader(shader, child) shader = shader.GetNext() class ShaderBrowser(c4d.gui.TreeViewFunctions): def __init__(self, dlg): self._dlg = weakref.ref(dlg) def Load(self): doc = c4d.documents.GetActiveDocument() self.nodes = [] self.collection = MaterialCollection(doc, doc.GetMaterials()) for material in self.collection: shader = material.GetFirstShader() root = Node(material) while shader: child = Node(shader) root.AddChild(child) recurse_shader(shader, child) shader = shader.GetNext() self.nodes.append(root) def IsResizeColAllowed(self, root, userdata, lColID): if lColID == ID_NAME: return True return False def IsTristate(self, root, userdata): return False def GetColumnWidth(self, root, userdata, obj, col, area): """Measures the width of cells. Although this function is called #GetColumnWidth and has a #col, it is not only executed by column but by cell. So, when there is a column with items requiring the width 5, 10, and 15, then there is no need for evaluating all items. Each item can return its ideal width and Cinema 4D will then pick the largest value. Args: root (any): The root node of the tree view. userdata (any): The user data of the tree view. obj (any): The item for the current cell. col (int): The index of the column #obj is contained in. area (GeUserArea): An already initialized GeUserArea to measure the width of strings. Returns: TYPE: Description """ # The default width of a column is 80 units. width = 80 # Replace the width with the text width. area is a prepopulated # user area which has already setup all the font stuff, we can # measure right away. if col == ID_TYPE: ICON_SIZE = 16 TEXT_SPACER = 6 return area.DrawGetTextWidth(obj.GetTypeName()) + ICON_SIZE + TEXT_SPACER + 5 if col == ID_NAME: return area.DrawGetTextWidth(obj.GetName()) + 5 return width def IsMoveColAllowed(self, root, userdata, lColID): # The user is allowed to move all columns. # TREEVIEW_MOVE_COLUMN must be set in the container of AddCustomGui. return True def GetFirst(self, root, userdata): """ Return the first element in the hierarchy, or None if there is no element. """ return None if not self.nodes else self.nodes[0] def GetDown(self, root, userdata, obj): """ Return a child of a node, since we only want a list, we return None everytime """ children = obj.GetChildren() if children: return children[0] return None def GetNext(self, root, userdata, obj): """ Returns the next Object to display after arg:'obj' """ parent = obj.GetParent() nodes = parent.GetChildren() if parent is not None else self.nodes indx = nodes.index(obj) nxt = indx + 1 return nodes[nxt] if nxt < len(nodes) else None def GetPred(self, root, userdata, obj): """ Returns the previous Object to display before arg:'obj' """ parent = obj.GetParent() nodes = parent.GetChildren() if parent is not None else self.nodes indx = nodes.index(obj) prev = indx - 1 return nodes[prev] if 0 <= prev < len(nodes) else None def GetId(self, root, userdata, obj): """ Return a unique ID for the element in the TreeView. """ return hash(obj) def Select(self, root, userdata, obj, mode): """ Called when the user selects an element. """ doc = c4d.documents.GetActiveDocument() doc.StartUndo() if mode == c4d.SELECTION_NEW: for node in NodeIterator(self.nodes): node.Deselect() obj.Select() elif mode == c4d.SELECTION_ADD: obj.Select() elif mode == c4d.SELECTION_SUB: obj.Deselect() doc.EndUndo() c4d.EventAdd() def IsSelected(self, root, userdata, obj): """ Returns: True if *obj* is selected, False if not. """ return obj.IsSelected def SetCheck(self, root, userdata, obj, column, checked, msg): """ Called when the user clicks on a checkbox for an object in a `c4d.LV_CHECKBOX` column. """ if checked: obj.Select() else: obj.Deselect() def IsChecked(self, root, userdata, obj, column): """ Returns: (int): Status of the checkbox in the specified *column* for *obj*. """ if obj.IsSelected: return c4d.LV_CHECKBOX_CHECKED | c4d.LV_CHECKBOX_ENABLED else: return c4d.LV_CHECKBOX_ENABLED def IsOpened(self, root, userdata, obj): """ Returns: (bool): Status If it's opened = True (folded) or closed = False. """ return obj.IsOpen def Open(self, root, userdata, obj, onoff): """ Called when the user clicks on a folding state of an object to display/hide its children """ doc = obj.obj.GetDocument() doc.StartUndo() doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj.obj) if onoff: obj.Open() else: obj.Close() doc.EndUndo() def GetName(self, root, userdata, obj): """ Returns the name to display for arg:'obj', only called for column of type LV_TREE """ return str(obj) def DrawCell(self, root, userdata, obj, col, drawinfo, bgColor): """ Draw into a Cell, only called for column of type LV_USER """ if col == ID_TYPE: ICON_SIZE = drawinfo["height"] TEXT_SPACER = 6 icon = obj.obj.GetIcon() drawinfo["frame"].DrawBitmap( icon["bmp"], drawinfo["xpos"], drawinfo["ypos"], 16, 16, icon["x"], icon["y"], icon["w"], icon["h"], c4d.BMP_ALLOWALPHA) name = obj.GetTypeName() geUserArea = drawinfo["frame"] w = geUserArea.DrawGetTextWidth(name) h = geUserArea.DrawGetFontHeight() xpos = drawinfo["xpos"] + ICON_SIZE + TEXT_SPACER ypos = drawinfo["ypos"] + drawinfo["height"] drawinfo["frame"].DrawText(name, int(xpos), int(ypos - h * 1.1)) def DoubleClick(self, root, userdata, obj, col, mouseinfo): """ Called when the user double-clicks on an entry in the TreeView. Returns: (bool): True if the double-click was handled, False if the default action should kick in. The default action will invoke the rename procedure for the object, causing `SetName()` to be called. """ if col == ID_NAME: return False if col == ID_TYPE: mode = (c4d.ACTIVEOBJECTMODE_SHADER if isinstance(obj.obj, c4d.BaseShader) else c4d.ACTIVEOBJECTMODE_MATERIAL ) c4d.gui.ActiveObjectManager_SetObject( id=mode, op=obj.obj, flags=c4d.ACTIVEOBJECTMANAGER_SETOBJECTS_OPEN, activepage=c4d.DescID() ) return True return True def SetName(self, root, userdata, obj, name): """ Called when the user renames the element. `DoubleClick()` must return False for this to work. """ doc = c4d.documents.GetActiveDocument() doc.StartUndo() doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj.obj) obj.SetName(name) doc.EndUndo() c4d.EventAdd() def DeletePressed(self, root, userdata): "Called when a delete event is received." doc = c4d.documents.GetActiveDocument() doc.StartUndo() for node in reversed(list(NodeIterator(self.nodes))): if not node.IsSelected: continue parent = node.GetParent() nodes = parent.GetChildren() if parent is not None else self.nodes nodes.remove(node) doc.AddUndo(c4d.UNDOTYPE_DELETE, node.obj) node.obj.Remove() doc.EndUndo() c4d.EventAdd() class ShaderBrowserDialog(c4d.gui.GeDialog): def CreateLayout(self): self.SetTitle("Shader Browser") customgui = c4d.BaseContainer() customgui.SetBool(c4d.TREEVIEW_BORDER, c4d.BORDER_THIN_IN) customgui.SetBool(c4d.TREEVIEW_HAS_HEADER, True) # True if the tree view may have a header line. customgui.SetBool(c4d.TREEVIEW_HIDE_LINES, False) # True if no lines should be drawn. customgui.SetBool(c4d.TREEVIEW_MOVE_COLUMN, False) # True if the user can move the columns. customgui.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True) # True if the column width can be changed by the user. customgui.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True) # True if all lines have the same height. customgui.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True) # Alternate background per line. customgui.SetBool(c4d.TREEVIEW_CURSORKEYS, True) # True if cursor keys should be processed. customgui.SetBool(c4d.TREEVIEW_NOENTERRENAME, False) # Suppresses the rename popup when the user presses enter. self._treegui = self.AddCustomGui(1000, c4d.CUSTOMGUI_TREEVIEW, "", c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 300, 150, customgui) return True def InitValues(self): layout = c4d.BaseContainer() layout.SetLong(ID_CHECKBOX, c4d.LV_CHECKBOX) layout.SetLong(ID_NAME, c4d.LV_TREE) layout.SetLong(ID_TYPE, c4d.LV_USER) self._treegui.SetLayout(3, layout) self._treegui.SetHeaderText(ID_CHECKBOX, "Check") self._treegui.SetHeaderText(ID_TYPE, "Type") self._treegui.SetHeaderText(ID_NAME, "Name") self._browser = ShaderBrowser(self) self._browser.Load() self._treegui.SetRoot(self._treegui, self._browser, None) self._treegui.Refresh() return True def CoreMessage(self, id, msg): if id == c4d.EVMSG_CHANGE: doc = c4d.documents.GetActiveDocument() materials = doc.GetMaterials() collection = MaterialCollection(doc, materials) nodes = self._browser.nodes[:] node_uuids = {bytes(node.obj.FindUniqueID(c4d.MAXON_CREATOR_ID)): node for node in nodes} # Compare the stored length of Material Collection to a newly allocated. # If they dont match a material must have been added or removed. # Better update the Treeview. if len(self._browser.collection.Markers) != len(collection.Markers): print("Material must have been added or removed.") self.InitValues() self._treegui.Refresh() # Compare the dirty states of the stored Material Collection with a newly allocated. # If the dirts states dont match something in the container of at least one material # must have been changed. Better update the Treeview. olddirty = self._browser.collection.Dirty for dirty in collection.Dirty: if dirty not in olddirty: print("something must have changed") self.InitValues() # self._browser.Load() self._treegui.Refresh() break return c4d.gui.GeDialog.CoreMessage(self, id, msg) def main(): global dialog dialog = ShaderBrowserDialog() dialog.Open(c4d.DLG_TYPE_ASYNC, defaulth=600, defaultw=600) if __name__ == "__main__": main()
[URL-REMOVED] @maxon: This section contained a non-resolving link which has been removed.
-
Hello @herrmay,
The problem with calling self.InitValues() on every change in a document is that I can not select items in the Treeview any longer because that in itself is a change which calls c4d.EVMSG_CHANGE. Which in turn invokes self.InitValues() again. I guess this is what you meant when you were talking about screwing up selection states.
No, I was not thinking of this kind of feedback loop in particular, but you are encountering here the frequent problem of being too loose with
EVMSG_CHANGE
and then landing in a feedback loop of your own changes.I actually read the posts you mentioned about c4d.MAXON_CREATOR_ID prior to this thread ...
Yes, you did things correctly. Although I would say the solution is a bit overengineered in this case, you do not really need a managing interface here. You were also missing the part where you track the data dirtyness of materials. You could also expand the whole thing to shaders if you wanted to, but I think there are no cases where a shader is dirty without its material not being dirty, but I might be wrong. If so, you would have also to track shaders. My less fancy solution would be:
class ShaderBrowserDialog(c4d.gui.GeDialog): # (f_hoppe): Start of changes def __init__(self) -> None: """ """ # Stores the material scene state managed by the current tree view instance for dirty comparisons self._materialStateCache: set[tuple(bytes, int)] = {} super().__init__() # (f_hoppe): End of changes # ... # (f_hoppe): Start of changes def IsMaterialDirty(self) -> bool: """Returns if the state of all materials in the active document is different from the state currently tracked by the tree view. Will also update the tracked state. """ doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument() materialState: set[tuple(bytes, int)] = { (bytes(mat.FindUniqueID(c4d.MAXON_CREATOR_ID)), mat.GetDirty(c4d.DIRTYFLAGS_DATA)) for mat in doc.GetMaterials() } res: bool = materialState != self._materialStateCache self._materialStateCache = materialState return res def CoreMessage(self, id, msg): """ """ if id == c4d.EVMSG_CHANGE and self.IsMaterialDirty(): print ("Updating material data.") self.InitValues() return c4d.gui.GeDialog.CoreMessage(self, id, msg) # (f_hoppe): End of changes
The problem is that every time the Treeview is updated the folding of items isn't respected.
That was what I meant by having problems with selection states In the previous example, we dealt with
BaseObject
instances which naturally have a folding state which is stored in the actual (object) node itself. In this code, you useNode._open
in the tree node type to effectively store that state. When you throw away all the data on a "something is dirty about materials"-event, you also throw away that folding state. You are also still using here the folding flag of nodesBIT_OFOLD
but in the crucial bit, yourIsOpen()
, you are not relying on it.def __init__(self, obj): self.obj = obj self. Children True @property def IsOpen(self): return self._open def Open(self): self._open = True self.obj.DelBit(c4d.BIT_OFOLD) def Close(self): self._open = False self.obj.SetBit(c4d.BIT_OFOLD)
I went here for a fix which stores arbitrary data in a global lookup table over node UUIDs as this a more universal fix. One could also fix this with folding bits to have the data stored in the materials and shaders themselves (I think, did not try). My fix is also a bit from the tribe of "sledgehammer-fixes", as
g_isopen_states
will grow over the lifetime of a Cinema 4D instance, and could end up taking up a few kilobytes if you really go to town with materials and shaders in a scene. More fancy book keeping could help but is not worth the hassle IMHO.class Node(object): """Class which represent a an item in our Tree.""" # (f_hoppe): Start of changes # A class bound look up table for the folding state of all ever encountered unique C4DAtom # instances passed to Node.__init__ as `obj`. g_isopen_states: dict[bytes, bool] = {} def __init__(self, obj): self.obj: c4d.C4DAtom = obj self.children = [] self._selected = False self._parent = None # Hash the node into its UUID. self._uuid: bytes = bytes(self.obj.FindUniqueID(c4d.MAXON_CREATOR_ID)) # Not needed anymore. # self._open = False # (f_hoppe): End of changes # ... # (f_hoppe): Start of changes @property def IsOpen(self): """Returns the folding state of the node. The state is stored over the hash of the attached atom on the class interface. Not the most elegant design, but it will work :) """ state: bool = Node.g_isopen_states.get(self._uuid, None) if state is None: Node.g_isopen_states[self._uuid] = False return False return state def Open(self): state: bool = Node.g_isopen_states.get(self._uuid, None) state = False if state is None else not state Node.g_isopen_states[self._uuid] = state def Close(self): Node.g_isopen_states[self._uuid] = False # (f_hoppe): End of changes
Cheers,
FerdinandFile: shader_viewp.py
I have fenced in my changes with (comments marked byf_hoppe
).Result:
We see the selection states being maintained due to being stored over UUIDs and also the data model of the tree view updating when one of the materials becomes data dirty by for example a paramater being changed.
-
Hello @ferdinand,
sorry for coming back this late. Work kept me quite busy.
@ferdinand so, I had a play with your code example. It works like a charm and also seems to have a lot of potential for all kinds of update/dirty checking issues.
Thanks again @ferdinand for your help, the effort you always put into answering our questions and the eloborate examples you come up with!
Cheers,
Sebastian -