Hi guys,
I'm currently testing around with the Treeview GUI. For that I'm using the example made by Niklas Rosenstein. But instead of building a Treeview for objects I changed the code to build a Treeview for materials and their shaders.
TL;DR
The Treeview is working so far as expected. The problem I encounter is when I try to rename any item in the Treeview. Cinema reliably crashes when I do so.
I don't know if this is a bug or - what's more likely - something is done wrong with my implementation of the Treeview and its methods.
I also tried to build the Treeview by wrapping every material with its shaders in a custom class. While that works in regards to the renaming procedure (Cinema then does not crash), the Treeview does not refresh correctly. That is, deleting any material or shader in the document does not get reflected in the Treeview. That is why I went back to the example below.
Here's the code I'm using. I hope someone can enlighten me on this one.
Cheers,
Sebastian
import c4d
import weakref
class Hierarchy(c4d.gui.TreeViewFunctions):
def __init__(self, dlg):
# Avoid a cyclic reference.
self._dlg = weakref.ref(dlg)
def GetFirst(self, root, userdata):
"""
Return the first element in the hierarchy. This can be any Python
object. With Cinema 4D nodes, it is easy to implement, but it could
as well be a Python integer which you can use as an index in a list.
Returns:
(any or None): The first element in the hierarchy or None.
"""
doc = c4d.documents.GetActiveDocument()
return doc.GetFirstMaterial()
def GetDown(self, root, userdata, node):
"""
Returns:
(any or None): The child element going from *node* or None.
"""
if isinstance(node, c4d.Material):
return node.GetFirstShader()
return node.GetDown()
def GetNext(self, root, userdata, node):
"""
Returns:
(any or None): The next element going from *node* or None.
"""
return node.GetNext()
def GetPred(self, root, userdata, node):
"""
Returns:
(any or None): The previous element going from *node* or None.
"""
return node.GetPred()
def GetName(self, root, userdata, node):
"""
Returns:
(str): The name to display for *node*.
"""
return node.GetName()
def Open(self, root, userdata, node, opened):
"""
Called when the user opens or closes the children of *obj* in
the Tree View.
"""
doc = c4d.documents.GetActiveDocument()
doc.StartUndo()
doc.AddUndo(c4d.UNDOTYPE_CHANGE_SELECTION, node)
if opened:
node.SetBit(c4d.BIT_OFOLD)
else:
node.DelBit(c4d.BIT_OFOLD)
doc.EndUndo()
c4d.EventAdd()
def IsOpened(self, root, userdata, node):
"""
Returns:
(bool): True if *obj* is opened (expaneded), False if it is
closed (collapsed).
"""
return node.GetBit(c4d.BIT_OFOLD)
def DeletePressed(self, root, userdata):
"""
Called when the user right-click Deletes or presses the Delete key.
Should delete all selected elements.
"""
def delete_recursive(node):
if node is None:
return
if node.GetBit(c4d.BIT_ACTIVE) == True:
doc.AddUndo(c4d.UNDOTYPE_DELETE, node)
node.Remove()
return
for child in node.GetChildren():
delete_recursive(child)
doc = c4d.documents.GetActiveDocument()
doc.StartUndo()
for mat in doc.GetMaterials():
delete_recursive(mat)
doc.EndUndo()
c4d.EventAdd()
def Deselect(self, doc):
for mat in iter_baselist(doc.GetFirstMaterial()):
doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, mat)
mat.DelBit(c4d.BIT_ACTIVE)
for shader in iter_children(mat.GetFirstShader()):
doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, shader)
shader.DelBit(c4d.BIT_ACTIVE)
def Select(self, root, userdata, node, mode):
"""
Called when the user selects an element.
"""
doc = c4d.documents.GetActiveDocument()
doc.StartUndo()
doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, node)
if mode == c4d.SELECTION_NEW:
self.Deselect(doc)
node.SetBit(c4d.BIT_ACTIVE)
elif mode == c4d.SELECTION_ADD:
node.SetBit(c4d.BIT_ACTIVE)
elif mode == c4d.SELECTION_SUB:
node.DelBit(c4d.BIT_ACTIVE)
doc.EndUndo()
c4d.EventAdd()
def IsSelected(self, root, userdata, node):
"""
Returns:
(bool): True if *obj* is selected, False if not.
"""
return node.GetBit(c4d.BIT_ACTIVE)
def IsResizeColAllowed(self, root, userdata, lColID):
if lColID > 2:
return True
return False
def IsTristate(self, root, userdata):
return False
def GetDragType(self, root, userdata, node):
"""
Returns:
(int): The drag datatype.
"""
return c4d.NOTOK
# def DragStart(self, root, userdata, node):
# """
# Returns:
# (int): Bitmask specifying options for the drag, whether it is
# allowed, etc.
# """
# return c4d.TREEVIEW_DRAGSTART_ALLOW | c4d.TREEVIEW_DRAGSTART_SELECT
def GetId(self, root, userdata, node):
"""
Return a unique ID for the element in the TreeView.
"""
return node.GetUniqueID()
def DoubleClick(self, root, userdata, node, 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_TREEVIEW:
return False
else:
return True
# Performing a rename on any item in the Treeview is crashing Cinema reliably.
def SetName(self, root, userdata, node, 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, node)
node.SetName(name)
doc.EndUndo()
c4d.EventAdd()
def DrawCell(self, root, userdata, node, col, drawinfo, bgColor):
"""
Called for a cell with layout type `c4d.LV_USER` or `c4d.LV_USERTREE`
to draw the contents of a cell.
"""
if col == ID_ICON:
icon = node.GetIcon()
drawinfo["frame"].DrawBitmap(
icon["bmp"], drawinfo["xpos"], drawinfo["ypos"],
16, 16, icon["x"], icon["y"], icon["w"], icon["h"], c4d.BMP_ALLOWALPHA)
if col == ID_TREEVIEW_TYPE:
name = node.GetTypeName()
geUserArea = drawinfo["frame"]
w = geUserArea.DrawGetTextWidth(name)
h = geUserArea.DrawGetFontHeight()
xpos = drawinfo["xpos"]
ypos = drawinfo["ypos"] + drawinfo["height"]
drawinfo["frame"].DrawText(name, xpos, ypos - h * 1.1)
if col == ID_TREEVIEW_CHANNEL:
if isinstance(node, c4d.Material):
return
if node == self.GetShader(c4d.MATERIAL_COLOR_SHADER):
name = "Color"
elif node == self.GetShader(c4d.MATERIAL_LUMINANCE_SHADER):
name = "Luminance"
elif node == self.GetShader(c4d.MATERIAL_DIFFUSION_SHADER):
name = "Diffusion"
else:
name = ""
if name != "":
geUserArea = drawinfo["frame"]
w = geUserArea.DrawGetTextWidth(name)
h = geUserArea.DrawGetFontHeight()
xpos = drawinfo["xpos"]
ypos = drawinfo["ypos"] + drawinfo["height"]
drawinfo["frame"].DrawText(name, xpos, ypos - h * 1.1)
def SetCheck(self, root, userdata, node, column, checked, msg):
"""
Called when the user clicks on a checkbox for an object in a
`c4d.LV_CHECKBOX` column.
"""
if checked:
node.SetBit(BIT_CHECKED)
else:
node.DelBit(BIT_CHECKED)
def checked_collect(op):
if op is None:
return
elif isinstance(op, c4d.documents.BaseDocument):
for obj in op.GetObjects():
for x in checked_collect(obj):
yield x
else:
if op.GetBit(BIT_CHECKED):
yield op
for child in op.GetChildren():
for x in checked_collect(child):
yield x
status = ', '.join(obj.GetName() for obj in checked_collect(node.GetDocument()))
self._dlg().SetString(1001, "Checked: " + status)
self._dlg()._treegui.Refresh()
def IsChecked(self, root, userdata, node, column):
"""
Returns:
(int): Status of the checkbox in the specified *column* for *obj*.
"""
val = node.GetBit(BIT_CHECKED)
if val == True:
return c4d.LV_CHECKBOX_CHECKED | c4d.LV_CHECKBOX_ENABLED
else:
return c4d.LV_CHECKBOX_ENABLED
def HeaderClick(self, root, userdata, column, channel, is_double_click, mouseX, mouseY, ua):
"""
Called when the TreeView header was clicked.
Returns:
(bool): True if the event was handled, False if not.
"""
c4d.gui.MessageDialog("You clicked on the '%i' header." % (column))
return True
def AcceptDragObject(self, root, userdata, node, dragtype, dragobject):
"""
Called when a drag & drop operation hovers over the TreeView to check
if the drag can be accepted.
Returns:
(int, bool)
"""
# if dragtype != c4d.DRAGTYPE_ATOMARRAY:
# return 0
# return c4d.INSERT_BEFORE | c4d.INSERT_AFTER | c4d.INSERT_UNDER, True
return 0
def GenerateDragArray(self, root, userdata, node):
"""
Return:
(list of c4d.BaseList2D): Generate a list of objects that can be
dragged from the TreeView for the `c4d.DRAGTYPE_ATOMARRAY` type.
"""
if node.GetBit(c4d.BIT_ACTIVE):
return [node, ]
def InsertObject(self, root, userdata, node, dragtype, dragobject, insertmode, bCopy):
"""
Called when a drag is dropped on the TreeView.
"""
if dragtype != c4d.DRAGTYPE_ATOMARRAY:
return # Shouldnt happen, we catched that in AcceptDragObject
for op in dragobject:
op.Remove()
if insertmode == c4d.INSERT_BEFORE:
op.InsertBefore(node)
elif insertmode == c4d.INSERT_AFTER:
op.InsertAfter(node)
elif insertmode == c4d.INSERT_UNDER:
op.InsertUnder(node)
return
def GetColumnWidth(self, root, userdata, node, 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_ICON:
return 5
if col == ID_TREEVIEW:
return area.DrawGetTextWidth(node.GetName()) + 5
if col in (ID_TREEVIEW_TYPE, ID_TREEVIEW_CHANNEL):
return area.DrawGetTextWidth(node.GetTypeName()) + 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 False
def GetColors(self, root, userdata, node, pNormal, pSelected):
"""
Retrieve colors for the TreeView elements.
Returns:
(int or c4d.Vector, int or c4d.Vector): The colors for the normal
and selected state of the element.
"""
usecolor = node[c4d.ID_BASEOBJECT_USECOLOR]
if usecolor == c4d.ID_BASEOBJECT_USECOLOR_ALWAYS:
pNormal = node[c4d.ID_BASEOBJECT_COLOR]
return pNormal, pSelected
# Context menu entry for "Hello World!"
ID_HELLOWORLD = 355435345
def CreateContextMenu(self, root, userdata, node, lColumn, bc):
"""
User clicked with the right mouse button on an entry so
we can here enhance the menu
"""
bc.SetString(self.ID_HELLOWORLD, "Set Original Name")
def ContextMenuCall(self, root, userdata, node, lColumn, lCommand):
"""
The user executes an entry of the context menu.
Returns:
(bool): True if the event was handled, False if not.
"""
if lCommand == self.ID_HELLOWORLD:
doc = c4d.documents.GetActiveDocument()
doc.StartUndo()
doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, node)
node.SetName(node.GetTypeName())
doc.EndUndo()
c4d.EventAdd()
return True
return False
def SelectionChanged(self, root, userdata):
"""
Called when the selected elements in the TreeView changed.
"""
print("The selection changed")
def Scrolled(self, root, userdata, h, v, x, y):
"""
Called when the TreeView is scrolled.
"""
self._dlg().SetString(1002, ("H: %i V: %i X: %i Y: %i" % (h, v, x, y)))
class TestDialog(c4d.gui.GeDialog):
_treegui = None
def CreateLayout(self):
# Create the TreeView GUI.
customgui = c4d.BaseContainer()
customgui.SetBool(c4d.TREEVIEW_BORDER, True)
customgui.SetBool(c4d.TREEVIEW_HAS_HEADER, True)
customgui.SetBool(c4d.TREEVIEW_HIDE_LINES, False)
customgui.SetBool(c4d.TREEVIEW_MOVE_COLUMN, True)
customgui.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True)
customgui.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True)
customgui.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True)
self._treegui = self.AddCustomGui(
1000, c4d.CUSTOMGUI_TREEVIEW, "", c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT,
300, 300, customgui)
if not self._treegui:
print("[ERROR]: Could not create TreeView")
return False
self.AddMultiLineEditText(1001, c4d.BFH_SCALEFIT | c4d.BFV_SCALE | c4d.BFV_BOTTOM, 0, 40)
self.AddEditText(1002, c4d.BFH_SCALEFIT, 0, 25)
return True
def CoreMessage(self, id, msg):
if id == c4d.EVMSG_CHANGE:
# Update the treeview on each change in the document.
self._treegui.Refresh()
return True
def InitValues(self):
# Initialize the column layout for the TreeView.
layout = c4d.BaseContainer()
layout.SetLong(ID_CHECKBOX, c4d.LV_CHECKBOX)
layout.SetLong(ID_ICON, c4d.LV_USER)
layout.SetLong(ID_TREEVIEW, c4d.LV_TREE)
layout.SetLong(ID_TREEVIEW_TYPE, c4d.LV_USER)
layout.SetLong(ID_TREEVIEW_CHANNEL, c4d.LV_USER)
self._treegui.SetLayout(5, layout)
# Set the header titles.
self._treegui.SetHeaderText(ID_CHECKBOX, "Check")
self._treegui.SetHeaderText(ID_ICON, "Icon")
self._treegui.SetHeaderText(ID_TREEVIEW, "Name")
self._treegui.SetHeaderText(ID_TREEVIEW_TYPE, "Type")
self._treegui.SetHeaderText(ID_TREEVIEW_CHANNEL, "Channel")
self._treegui.Refresh()
# Don't need to store the hierarchy, due to SetRoot()
root = doc = c4d.documents.GetActiveDocument()
data_model = Hierarchy(self)
self._treegui.SetRoot(root, data_model, None)
self.SetString(1002, "Scroll values")
return True
class MenuCommand(c4d.plugins.CommandData):
dialog = None
def Execute(self, doc):
"""
Just create the dialog when the user clicked on the entry
in the plugins menu to open it
"""
if self.dialog is None:
self.dialog = TestDialog()
return self.dialog.Open(
c4d.DLG_TYPE_ASYNC, PLUGIN_ID, defaulth=600, defaultw=600)
def RestoreLayout(self, sec_ref):
"""
Same for this method. Just allocate it when the dialog is needed.
"""
if self.dialog is None:
self.dialog = TestDialog()
return self.dialog.Restore(PLUGIN_ID, secret=sec_ref)
def main():
# Using a global variable here for testing only.
global dialog
dialog = TestDialog()
dialog.Open(c4d.DLG_TYPE_ASYNC, defaulth=600, defaultw=600)
if __name__ == "__main__":
main()