Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Treeview does not refresh

    Cinema 4D SDK
    r20 python
    2
    5
    1.1k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • H
      HerrMay
      last edited by

      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.

      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @HerrMay
        last edited by ferdinand

        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,
        Ferdinand

        Result
        materials.gif

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • H
          HerrMay
          last edited by

          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 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.

          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 is True. 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.

          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @HerrMay
            last edited by ferdinand

            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 use Node._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 nodes BIT_OFOLD but in the crucial bit, your IsOpen(), 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,
            Ferdinand

            File: shader_viewp.py
            I have fenced in my changes with (comments marked by f_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.

            shader_browser_2.gif

            MAXON SDK Specialist
            developers.maxon.net

            H 1 Reply Last reply Reply Quote 1
            • H
              HerrMay @ferdinand
              last edited by

              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

              1 Reply Last reply Reply Quote 0
              • ferdinandF ferdinand referenced this topic on
              • First post
                Last post