using customgui listview

On 23/02/2018 at 13:00, xxxxxxxx wrote:

Hi,

I'm new-ish to guis in python for c4d.  Looking for documentation.

My goal is to implement a plugin with a list view in a dialog, similar to the texture manager (with the check/x buttons on each row).  I think I've successfully added a listview customgui, but can't find examples on how to interact with it.

self.AddCustomGui(1001, c4d.CUSTOMGUI_LISTVIEW, "myList", c4d.BFH_SCALEFIT, 300, 200)

Any guidance would be great.  I've checked both the python and cpp docs as sometimes you can find things in the latter to help with the former.  Might have just missed it completely, so apologies if I'm not being thorough enough.

Thanks.

--
Kevin

On 26/02/2018 at 04:04, xxxxxxxx wrote:

Hello KevinW,

First of all, this is actually not possible to create a ListView in python. But you can create a TreeView, wich is basicly a more complexe ListView.
So with that said, let's get's started !
The bascis concept is to create a CUSTOMGUI_TREEVIEW, then attach an instance of a c4d.gui.TreeViewFunctions.
Our instance of c4d.gui.TreeViewFunctions will then react to different functions according c4d.gui.TreeViewFunctions

But let's go step by step
First things is to create a GeDialog, and add a custom GUI. Ignore ListView() for the moment

class TestDialog(c4d.gui.GeDialog) :
    _treegui = None # Our CustomGui TreeView
    _listView = ListView() # Our Instance of c4d.gui.TreeViewFunctions
  
    def CreateLayout(self) :
        # Create the TreeView GUI.
        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, True) # 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, 300, customgui)
        if not self._treegui:
            print "[ERROR]: Could not create TreeView"
            return False
  
        self.AddButton(1001, c4d.BFH_CENTER, name="Add")
        return True

Then we need to initializes some values to this CustomGui. So we implement InitValues within our GeDialog:

    def InitValues(self) :
        # Initialize the column layout for the TreeView.
        layout = c4d.BaseContainer()
        layout.SetLong(ID_CHECKBOX, c4d.LV_CHECKBOX)
        layout.SetLong(ID_NAME, c4d.LV_TREE)
        layout.SetLong(ID_OTHER, c4d.LV_USER)
        self._treegui.SetLayout(3, layout)
  
        # Set the header titles.
        self._treegui.SetHeaderText(ID_CHECKBOX, "Check")
        self._treegui.SetHeaderText(ID_NAME, "Name")
        self._treegui.SetHeaderText(ID_OTHER, "Other")
        self._treegui.Refresh()
  
        # Set TreeViewFunctions instance used by our CUSTOMGUI_TREEVIEW
        self._treegui.SetRoot(self._treegui, self._listView, None)
        return True

Now that our GeDialog is created, we need to implement our ListView.
The purpose of this class is to manage some objects (as the Object manager does for example).
So we firstly need to create a custom Object which will represent an item in our list.

class TextureObject(object) :
    """
    Class which represent a texture, aka an Item in our list
    """
    texturePath = "TexPath"
    otherData = "OtherData"
    _selected = False
  
    def __init__(self, texturePath) :
        self.texturePath = texturePath
        self.otherData += texturePath
  
    @property
    def IsSelected(self) :
        return self._selected
  
    def Select(self) :
        self._selected = True
  
    def Deselect(self) :
        self._selected = False
  
    def __repr__(self) :
        return str(self)
  
    def __str__(self) :
        return self.texturePath

Note that in your case you may want to create an object from a BaseList2D, by doing RegisterNodeData, and which will allow you to support Drag and drop. But that's maybe something we can see in a future post.

Then let's start by implemeting our class derived from c4d.gui.TreeViewFunctions

class ListView(c4d.gui.TreeViewFunctions) :
  
    def __init__(self) :
        self.listOfTexture = list() # Store all objects we need to display in this list
  
        # Add some defaults values 
        t1 = TextureObject("T1")
        t2 = TextureObject("T2")
        t3 = TextureObject("T3")
        t4 = TextureObject("T4")
  
        self.listOfTexture.extend([t1, t2, t3, t4])

The whole concept of TreeView is to override some functions. So make sure to read the documentation to know which functions to override according your need.
So we will define some more general options for our TreeView:

    def IsResizeColAllowed(self, root, userdata, lColID) :
        return True
  
    def IsTristate(self, root, userdata) :
        return False
  
    def GetColumnWidth(self, root, userdata, obj, col, area) :
        return 80  # All have the same initial 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

Now, since everything is setup we have to tell which item is the first one. Then all our lists view will be process as you process the current ObjectManager (GetNext,GetDown).
To do it we override c4d.gui.TreeViewFunctions.GetFirst

    def GetFirst(self, root, userdata) :
        """
        Return the first element in the hierarchy, or None if there is no element.
        """
        rValue = None if not self.listOfTexture else self.listOfTexture[0]
        return rValue

Then we have to handle GetDown (for child object, since in our case it's a simple list, we return None).
GetNext which is the Object after the current Object, and GetPred wich is the Object before current Object.
We also have to override GetID in order to uniquely identify an object in our list.

    def GetDown(self, root, userdata, obj) :
        """
        Return a child of a node, since we only want a list, we return None everytime
        """
        return None
  
    def GetNext(self, root, userdata, obj) :
        """
        Returns the next Object to display after arg:'obj'
        """
        rValue = None
        currentObjIndex = self.listOfTexture.index(obj)
        nextIndex = currentObjIndex + 1
        if nextIndex < len(self.listOfTexture) :
            rValue = self.listOfTexture[nextIndex]
  
        return rValue
  
    def GetPred(self, root, userdata, obj) :
        """
        Returns the previous Object to display before arg:'obj'
        """
        rValue = None
        currentObjIndex = self.listOfTexture.index(obj)
        predIndex = currentObjIndex - 1
        if 0 <= predIndex < len(self.listOfTexture) :
            rValue = self.listOfTexture[predIndex]
  
        return rValue
		
    def GetId(self, root, userdata, obj) :
        """
        Return a unique ID for the element in the TreeView.
        """
        return hash(obj)

In order to handle selection we have to override Select and IsSelected

    def Select(self, root, userdata, obj, mode) :
        """
        Called when the user selects an element.
        """
        if mode == c4d.SELECTION_NEW:
            for tex in self.listOfTexture:
                tex.Deselect()
            obj.Select()
        elif mode == c4d.SELECTION_ADD:
            obj.Select()
        elif mode == c4d.SELECTION_SUB:
            obj.Deselect()
  
    def IsSelected(self, root, userdata, obj) :
        """
        Returns: True if *obj* is selected, False if not.
        """
        return obj.IsSelected

As you may be aware, in our GeDialog::InitValues we define some columns with different flags (LV_CHECKBOX, LV_TREE and LV_USER). Each one get a different implementation.
So let's start with LV_CHECKBOX wich stand for check box.
In order to make it work we have to override SetCheck and IsChecked
Note that I used the same variable for selection, so if you select an item it will also check them, and if you check an item it will select it.

    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

Then LV_TREE element will check for Object name by calling GetName

    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) # Or obj.texturePath

And finally LV_USER allow us to do some drawing function, as you will do if you are in a GeUserArea. Use DrawCell to draw anything you want (in our case a text). Take a look at DrawInfo-Dict

    def DrawCell(self, root, userdata, obj, col, drawinfo, bgColor) :
        """
        Draw into a Cell, only called for column of type LV_USER
        """
        if col == ID_OTHER:
            name = obj.otherData
            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) # *1.1 ugly trick to make it nicer

Then few stuffs can be usefull, like to trigger some scripts when you double click by overriding DoubleClick
And also manager Delete key by overriding DeletePressed

    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.
        """
        c4d.gui.MessageDialog("You clicked on " + str(obj))
        return True
  
    def DeletePressed(self, root, userdata) :
        "Called when a delete event is received."
        for tex in reversed(self.listOfTexture) :
            if tex.IsSelected:
                self.listOfTexture.remove(tex)

And that's it, for our ListView, it's pretty basic but when you understand the concept is very easy, just take a look at all functions available for TreeViewFunctions.

Additionally you may want to trigger some changes from GeDialog to your list, then remember we Create a button in our CreateLayout, now let's create his interaction.

    def Command(self, id, msg) :
        # Click on button
        if id == 1001:
            # Add data to our DataStructure (ListView)
            newID = len(self._listView.listOfTexture) + 1 
            tex = TextureObject("T{}".format(newID))
            self._listView.listOfTexture.append(tex)
  
            # Refresh the TreeView
            self._treegui.Refresh()
  
        return True

And voila ! :)

Here the full example:
https://pastebin.com/hp9KpZjx

I hope everything make sense and answers to your initial question.
For the moment it's pretty basic, but I will see how I can improve it in order to also demonstrate how to support Drag and Drop.

Finally you can find some usefull informations in C++:
TreeView made simple - Part 1
Treeview made simple – Part 2

Cheers,
Maxime

On 26/02/2018 at 06:16, xxxxxxxx wrote:

This is super helpful Maxime, thank you very much!

This more then answers all my questions, and really makes a lot things much more clear.  This is more involved then I expected, but makes sense with your explanations.  Thanks very much.  I imagine this would be very useful to others as well!

--
Kevin