TouchDesigner | The Big Badass Lister | Part 2/3

THE BIG BADASS LISTER

Part 2 of 3

Well hello there! And welcome back for another installment of “I didn’t know how much I needed lister in my life.” Last week we looked at the basics for lister, getting started, and finding your bearings. This week we’ll take a closer look at some of the more nuanced pieces of lister. How you can add buttons to your lists, substitute images, create a roll-over button table, and work with data structures beyond just tables. If Part 1 was our beginner’s look at lister, this is our intermediate adventure in working with this component. 

Some caveats this week – in Part one all of the work we did with lister didn’t require any familiarity with Python. For some of the more intermediate use cases we do end up doing a little scripting to get exactly the results we want. Don’t worry – you don’t have to be a Pythonista to make these little bits of magic work, a little Python can go a long way in TouchDesigner especially with lister. 

Working with Tables

There are lots of ways we can work with tables with lister – and before we start to look at the more complex examples, let’s begin with some simple and practical examples. We’ll look at adding a button to our to our lister’s rows so we can remove an item, how to create an array of buttons, how to work with a TOP path to set a static button look, how to dynamically update our buttons in a single toggle style, and how update our buttons based on their toggle state.

Adding a Button Column

We’re going to use many of the techniques we’ve already learned to style our lister, so many of our first steps should feel very familiar. We’ll also start with the same simple table DAT we used previously.

Input Table DAT

This time we’re going to use an extra parameter on the lister COMP so we can use a referenced input table DAT rather than connecting our table directly to lister. We’re also going to set lister so it will automatically update the contents of our input table. 

Update Table Par

Looking at our colDefine Table, the first two columns should look very similar to our pervious configurations. In this case our third column is the one to pay closer attention to. Here we’re adding a column that doesn’t currently exist in our input table, setting it’s contents to be a constant value of “X” and setting the sourceDataMode to be “button”. 

colDefine Table

The look of our button is going to be derived from the three TOPs labeled “button” “buttonRoll” and “buttonPress”. These TOPs hold all of the display information for what will happen to our column in it’s default display mode, when a mouse rolls over a row in the column, and when we press on a row in that column. 

Button Looks

Our resulting lister with the configuration above should look like this:

Styled Lister

This is great, but what can we do with this configuration? There are some exciting pieces to look at right away. First, if we look at our colDefine Table we’ll see that for column 2 we set this to be editable with the value 2 – this means if we double click on one of those cells we can update the value. We also turned on the “Auto-sync Input Table” parameter, which means if we update any values in our table those changes will be pushed back to our input table. 

This is great, but let’s do something with this lovely button we’ve added to our lister. We can look at some simple actions that we might accomplish here by opening up the lister’s callbacks. We can open up the callbacks by clicking on the parameter on lister called “Edit Callbacks”.

Edit Callbacks

Here in the lister callbacks we’ll make a few changes. What I want to do is to delete a row from our lister when we click the “X” button. To do this we need to know a few things – where is the lister that holds all of the data, which column have I clicked on, and which row do I want to delete. It’s not too hard to figure out these pieces. We can determine which lister we’re working on by looking at which component our listerConfig base is docked to. We can then find the row and column from the info object that’s passed to the lister callbacks. Finally we can use the DeleteRows() method from the lister extension to remove a row from our lister. Here’s the handful of python we’ll need to do this:

def onClick(info):

	# find our row and column
	clickedCol 	= info.get('colName')
	clickedRow 	= info.get('row')
	owner 		= info.get('ownerComp')

	print(info)

	# only if we've clicked the column that's named 'close'
	if clickedCol == 'close':
		# delete the row
		owner.DeleteRows([clickedRow])
		
	else:
		pass

The result should be a lister than we we click the “X” it removes the selected row:

Updating Lister

A quick overview of what’s happening in the callback – here we’re using the “onClick” method from the available callbacks. Unlike TouchDesigner’s built in ops, that start by showing you all of the available callbacks, for lister you need to check the documentation to see all of the available callbacks. When we click on a cell in lister we start by setting a few variables: our owner COMP, what row was clicked, and what column was clicked. Next we do a quick test to make sure the column that the command came from was the close column. This name comes from our colDefine Table. If the cell we clicked on is in the close column – which is what holds our buttons, the we use the provided method with lister “DeleteRows()” to remove. 

Creating a Button Table

Similar to the technique for adding a button as a single column, we can also think of our entire lister as being a set of buttons. For this example we’ll work with a slightly different input table DAT:

Input Table DAT

Our lister pars are also a little different for this example:

Lister Pars

Our colDefine table will hold a large portion of our configuration information for this approach. The set-up here looks like:

colDefine Table

I took some time to style my button TOPs – though this works even if you don’t style them:

Button Looks

Our resulting lister of buttons looks like this:

lister of Buttons

Let’s add one extra step so we can change a table when we press a button. Let’s add a table DAT to our network and call it table_info. Let’s set it up to have three rows and two columns. The rows should be named: row, col, text.

Adding an info table

We’ll open up our callbacks again, and this time we want to update our table_info with the row, column, and text that we’ve clicked on. We can get this information from the info object. Here’s a look at the python we’ll need to make this work:

targetTable = op('../table_info')

def onClick(info):
	clickedRow = info.get('row')
	clickedCol = info.get('col')
	cellText = info.get('cellText')

	targetTable['row', 1] = clickedRow
	targetTable['col', 1] = clickedCol
	targetTable['text', 1] = cellText

Now when we click on a cell in our lister our table updates with the changes.

TOP Path Example 1 – Press and Roll Changes

So far we’ve just used the button cellLook. We can achieve a similar effect by using a setting a topPath for the look of our cell. The idea here is that we have an image in a TOP that we’d like to place in a cell based on a set of possible variables. The easiest would be to change the image based on if the mouse is over the cell, or if we’ve pressed on the cell. That can be a little hard to write our minds around, so let’s look at an example to see this in action. 

We’ll start with a simple table DAT that has some information we’d like to display:

Input Table DAT

Next, let’s think about what kind of button interaction we want to create. For this example, let’s just look at how we might change the view of our button when we interact with lister – just a visual cue that we’ve clicked on a button. 

So just like with our previous example that used a TOP Path to change the contents of a cell with the provided TOPs, we can create our own TOPs for this. Let’s start by creating a button look, a roll, and a press look. Here’s what my network looks like:

Button Looks

Here that’s a top called “btnImage” along with tops that start with that same name and are appended with “Roll” and “Press.” This is important, because lister relies on the names of our TOPs to order to know which to display at what time. 

Next let’s look at the colDefine Table:

colDefine Table

Here we can see that our sourceDataMode is empty – which is exactly what we want in this case. We don’t want to display anything in particular, just our image. Next in our topPath row we can see that ‘btnImage’ is what’s describing which TOPs lister is going to use to fill in these cells. One final note on formatting here – when working with TOPs that have images in them I generally prefer to make sure that I don’t allow my cells to stretch. This can make sure that you know the exact dimensions of your cell, and can format your texture operators accordingly. In this case, the dimensions for our cells are going to be the height of the top called master – which is used to define a number of characteristics about our lister, and from the colDefine table row called “width”. 

The result from our configuration here is a lister whose buttons change color when we mouse over them and when we click them:

roll and press mechanic

But what if you don’t want to just use Roll and Press?!

That’s a great question, and there’s a solution here. Let’s look at a static example first, and then we’ll start to update our lister’s images.

One approach is to think about putting data in a column in a DAT table that you don’t need to display, but can instead use a reference for the state of your row. For example, let’s look at a table where we’ve added another column called ‘state’. In this column I’m just going to set state to be True if col2 is greater than 3.

input Table DAT

In the colDefine table we’re going to make a few adjustments. Here we’ll again use a topPath but we want to make sure that our sourceDataMode is ‘blank.’ This will keep the lister cell empty, even though it holds some data that we’re going to use. Next in our topPath we’re going to use ‘btnImage*’. In this case the * is going to act as a pattern matching mechanism; this matches the name of our tops to the contents of that column. Our state column had values that were “True” and “False” – this means that if I create TOPs with the names ‘btnImageTrue’ and ‘btnImageFalse’ they’ll be matched to display in the cells.

Let’s first look at the TOPs we need in our listerConfig:

Button Looks

Between our colDefine table and our additional TOPs, what we end up with should look like this:

Input Table DAT and lister

This is great for a scenario where you have some internal logic that’s updating a table that you want to see in a list, but don’t want to change directly. This could be online / offline status, or any number of other kinds of information. 

But… what if you do want your interaction with the table to update it’s display? Let’s look at that next.

TOP Path Example 2 – Updating a Single Cell

In some cases you just want to toggle the state of a single cell. We already have all the ingredients we need for this spell, we just have to put our magic words in the right order. 

We’ll start by setting up our input table:

Input Table DAT

Next let’s add some additional TOPs in our listerConfig to act as our buttons:

Button Looks

Next let’s make sure our colDefine table is setup correctly:

colDefine Table

Our last step is to fix-up our callbacks. We have all the ingredients in place, now we just need to set-up the logic that will handle how our buttons change. Our callbacks should look like this:

def onSelectRow(info):

	# find our input table DAT
	targetTable = info.get('ownerComp').par.Inputtabledat.eval()
	
	# find our row
	row = info.get('row')

	# check the contents of our cell - and set them to be the 
	# opposite of their current state
	stateVal = targetTable[row, 'state'].val

	if stateVal == 'True':
		updateVal = 'False'
	else:
		updateVal = 'True'	

	targetTable[row, 'state'] = updateVal

The big idea is that we check to see if the contents of our target cell are “True” or “False” and then swap them accordingly.

Toggle buttons in lister

This approach works great for a toggle style system, where any given row can be on or off. But… what about if only one row can be on? We’ll use all the same mechanics, and look at that next! 

TOP Path Example 2 – Updating a Whole Column

For this example you can set-up your input table and your colDefine table the same way. I’m going to change up the look of my buttons, but feel free to use the styles of your choice:

Button Looks

The real fun will come with our callbacks:

# find the intput table DAT
inputTable =  info.get('ownerComp').par.Inputtabledat.eval()

# find the data from the column called 'state'
stateCol = inputTable.col('state')  

# replace the contents of the colum
newStateData = [True if eachCell.row == info.get('row') else False for eachCell in stateCol[1:]]

# insert the header into the column
newStateData.insert(0, 'state')

# overwrite the column in the input table
inputTable.replaceCol('state', newStateData)

Unlike our previous approach where we just changed one cell, here we pull the whole state column. We then make sure the only cell that has a “True” value is the one we clicked on, then we overwrite the column in the original table. This wouldn’t be great for DATs with lots of rows, but for small – medium sized lists this might help you map out that “only one active” use case.

Single row toggle

Working with Lists…

Working with tables is cool, but part of what makes lister particularly powerful is that it can also work with lists of other objects. Lists of lists, lists of dictionaries, even lists of python objects. We’re going to stay out of the deep waters here, but it’s worth a little exploration to see how you can use python lists with lister. 

A Single List

Before we get started, let’s look first at how we can pass lister a regular python list as a data structure. 

On lister’s Advanced page there’s a parameter called Raw Data:

Raw Data parameter

We can fill that parameter in directly, or we can write a little script to fill in that parameter. For practice, let’s write a little script that’s going to do that work for us. In a text DAT let’s write the following:

A simple python list
myList = [ 'Monday',
	'Tuesday',
	'Wednesday',
	'Thursday',
	'Friday',
	'Saturday',
	'Sunday']

op('lister').par.Rawdata = myList

Here we start by making a simple list, and then we set the Rawdata parameter of our lister to be this list. Easy. 

Let’s set-up the colDefine table for our lister to work with our python list:

colDefine Table

Because our raw data doesn’t have a header, here in our colDefine we can set an internal name for this column of data, as well as a display label for this column. We should now see a lister that looks like this:

simple list in lister

A List of Lists

This is a great start, but what if you wanted more than one column of data? With lister you can also structure your raw data as lists of lists. This is a little different than our last example, but as we take this apart this idea it should all make sense.

Let’s think about tables for a moment. We often think of tables as a 2D array, in fact in TouchDesigner we use this idea all of the time to describe how we want to either retrieve or set contents in a table – we need to know the cell’s address as a row and column location. If we need to transform the contents of a table into a python object we might start to pull apart the best way to structure this. As a data structure, lists have an inherent order – so we might use that idea transforming a table into something more pythonic. One way to organize this data would be to make a list of rows – where each row is also a list. 

Let’s look at that as an image rather than just words. The idea here is that a table organized like this in TouchDesigner:

A simple table

Could be arranged like this as pure python:

A table as a list of lists
listOfLists = [['1', '2', '3', '4'], 
		['5', '6', '7', '8'],
		['9', '10', '11', '12']]

Okay… so with that little bit of data organization unpacked, we can set up lister with a list of lists as a data source. Let’s use a little script again to set the parameter for Raw Data in lister:

Setting our Raw Data par
listOfLists = [['1', '2', '3', '4'], 
		['5', '6', '7', '8'],
		['9', '10', '11', '12']]

op('lister').par.Rawdata = listOfLists

Next let’s set up our colDefine table. This is where things start to get a little different from our previous work. Since our list of lists doesn’t have a “header” like our table might, we need to do a little more work to make sure that this will behave correctly. We need to specify the names for our columns in our lister, and we should set the sourceData mode for our lister. Lister will do it’s best to figure this out for us, but we’ll ultimately be happier if we set these manually as we’re learning. When working with a list of lists the sourceData is the index in our lists of rows that we want to display – it’s like a column in our constructed array of data.

colDefine Table

The resulting lister should look like this:

list of lists in lister

A List of Dictionaries

So far we’ve looked at using a single list as raw data, or a list of lists. But what if we had a list of dictionaries? Unlike lists, dictionaries don’t have an explicit order. Instead, dictionaries work as key and value pairs. We’re going to use this concept when we work with a list of dictionaries for our lister. Let’s start by first getting our data ready. 

A list of dictionaries
listOfDicts = [
	{'fruit': 'apple', 'quantity': 10, 'icon' : 'Apple'},
	{'fruit': 'pineapple','quantity': 5, 'icon' : 'Pineapple'},
	{'fruit': 'grape', 'quantity': 2, 'icon' : 'Grape'},
	{'fruit': 'cherry', 'quantity': 6, 'icon': 'Cherry'}
]
op('lister').par.Rawdata = listOfDicts

Here all of the dictionaries contain the same keys. We’re going to use the Keys in our dictionary to be the columns for our lister, and the values that correspond to those keys as the contents of each cell in the rows of our lister.

I want to have little icons for my example, so I’m going to add a few extra TOPs that will act as buttons in my listerConfig:

Button Looks

Finally, let’s update our colDefine table for our lister. Here it’s important to note that the sourceData for our columns refers to a key in the dictionaries of our list’s items:

colDefine table

The result should be a lister that looks like this:

A list of dictionaries in lister

A List of Python Objects

Finally, this brings us a list of python objects. If you’ve gotten this far, I’m going to assume that you have a handle on using classes in Python. If that sounds unfamiliar, that’s okay – first take a moment to do a little googling about python classes, and look at extensions in TouchDesigner. Both of those ideas are important to understanding this next example.

We can accomplish this example by using modules on demand in TouchDesigner, but this is likely a better candidate for working with an extension. The big idea is that we’ll have a class that acts as the manager of our list objects. Those list objects will all be instances of a class themselves. We’ll create an instance of our manager class with another text DAT so we’re not continuously re-initializing the class when we interact with lister. 

My objects are going to have a handful of attributes – I’m going to define these in a dictionary for now, but you might construct these on the fly. This might be looks, presets, scenes, or any other type of collection of data.

Looking at this we’ll see that there are two classes – our first class is our MangerMod – this holds a list of our objects, and does the work of filling the list of objects. Next we can see the myObj class. Right now this only sets the attributes of the objects. 

A list of python objects
refData = [
	{'name' : 'apple',
	'color' : (0.454, 0, 0, 1),
	'top' : op('iconApple')}, 
	{'name' : 'pineapple',
	'color' : (0.719, 0.319, 0, 1),
	'top' : op('iconPineapple')}, 
	{'name' : 'grape',
	'color' : (0.6009, 0.827, 0.4697, 1),
	'top' : op('iconGrape')}, 
	{'name' : 'cherry',
	'color' : (0.258, 0.124, 0.5, 1),
	'top' : op('iconCherry')}]

class ManagerMod:
	def __init__(self):
		self.myObjs = []
		self.fillObjs()
		pass

	def fillObjs(self):
		for each in enumerate(refData):
			newObj = mod.myMOD.myObj(each[0], each[1])
			self.myObjs.append(newObj)


class myObj:
	def __init__(self, index, infoDict):
		self.myIndex = index
		self.fruit 	= infoDict.get('name')
		self.color 	= infoDict.get('color')
		self.icon = infoDict.get('top')

Next we create an instance of our manager class in a text DAT:

modules on demand

We can now target the myObjs member of our instance in the Raw Data parameter of our lister:

Python object list in the Raw Data parameter

Okay… now we’re ready to have some fun. Let’s look at our colDefine table in lister:

colDefine table

Notice how in sourceData we now use the name of the attribute of our python object. Our resulting lister looks like:

list of python objects

Wooooo!

At this point you might be rightly wondering why on earth you’d want to do this much work if you could just use a table. That’s a fair question, part of that is difficult to show in simple examples. That said, it’s not hard to imagine a situation where all of your critical data is stored in an extension, and rather than writing that to a table DAT (and then keeping it synced with your extension) it would be easier to just reference that in a lister directly. That’s part of the magic of this variation on using lister – you can use this to directly display pythonic pieces that might otherwise be more frustrating or time consuming to write out into a UI friendly data structure. 

This week we looked at a TON of variations in using lister – from simple alterations, to more complex uses of python. You can find the examples from list post here:

Big Bad Ass Lister Part 2 | Examples

Next week we’ll hear from the puppet master himself – Ivan – about the Dark Dank secrets of lister. 

Happy programming – and Lister…ing