We’ve looked at a few examples of Nuke code in my previous posts, and hopefully you’re starting to get around to reading Python code.
Today I’d like to take you through the whole process of making a small python plug-in.

We will start by defining the project, think about different solutions, define the main structure of the script, then code, test and debug 

The project:

We will give a big update to a tiny tool I made a long time ago and could hopefully become more useful than it proved to be.
It was a small menu containing a few presets for node colors.

colorPresets

And here was the code for it:

def batchTileColor(nukeHex = None):
    '''
    Changes the color of multiple nodes at once
    '''
    if nukeHex == None:
        nukeHex = nuke.getColor()
    for node in nuke.selectedNodes():
        node.knob('tile_color').setValue(nukeHex)

menu = nuke.toolbar("Nodes")
colorMenu = menu.addMenu( 'Color Nodes', icon="color_node.png")
colorMenu.addCommand("Red", lambda: batchTileColor(4278190335))
colorMenu.addCommand("Green", lambda: batchTileColor(16711935))
colorMenu.addCommand("Blue", lambda: batchTileColor(65535))
colorMenu.addCommand("Cyan", lambda: batchTileColor(16777215))
colorMenu.addCommand("Magenta", lambda: batchTileColor(4278255615))
colorMenu.addCommand("Yellow", lambda: batchTileColor(4294902015))
colorMenu.addCommand("Custom", lambda: batchTileColor())

There was nothing fancy, you should be able to grasp most of it by yourself.
I started by defining a function, that I called batchTileColor, taking one optional argument (Made optional by assigning it a default value).
The argument I’m expecting is an INT called nukeHex, although I’m taking a risk by never checking in my code that the user actually did provide an INT. It was okay as the target user was myself, and I wasn’t planning to call that function directly from command line, but that wouldn’t be wise for a released piece of code.

All the function does is: If no value has been passed, call the nuke command getColor() (which opens a color panel), then assign the color to the knob ’tile_color’, which is what defines the color of a knob in the node graph.

Then I just created the menu, assigned an icon to it, and populated it with a few commands/presets.

What’s wrong with it?

The colors I’ve set as presets aren’t actually colors I like to use. They are too bright for my liking. Also my needs are evolving with time, and I may want to add new presets to the list.
The way it currently is, I can’t change any preset without getting back in the code, and the format nuke uses for the tile_color is not easily readable or editable for a regular human.

What needs to change:

The first thing I need to change to make the tool useful, is to make it easy to save new presets from within Nuke.
It needs to use a panel familiar to Nuke users so that they don’t have to worry about finding the Hexadecimal value of the color.
That probably means a color picker, and a text field to enter the name of the preset.

In a second time (part 2 of this tutorial, coming soon) I’d like to be able to automatically pre-fill the name of the preset (similar to After-Effects automatically naming solid colors).
I would also like to add a way to delete or rename existing presets.

Let’s get started:

Now that we know our goals, it’s time to get started. I like to think about my project and layout it’s main structure before I start coding.
An easy way to do that is to write your program only with comments, to figure out the logic behind it.

#Function to change tile_color:
    #We can re-use part or all of the old one

#Function to create the Menu:
    #We need a variable to define what should be in the menu
    #A dic would be perfect, with the menu name as a key, and the value as a value.
    #We need to read this dic from a stored file somewhere

    #We need to call the menu to which we will add our "nodeColor" menu
    #and create the menu.

    #for each key in the dictionary:
        #add a menu entry

    #let's also add a "custom" menu entry (although nuke has one by default)
    #and also a menu entry "Add Color Preset"
    #Let's make that one call another function.

#Function to add a color preset:
    #Open a color picker panel.
    #If the user doesn't click cancel:
        #Open a Panel asking for a name.
        #If the user doesn't click cancel:
            #Verify the name isn't taken. Reopen the panel if it's already taken (loop)
                #If everything ok:
                    #add entry to the dictionary
                    #rebuild the menu with the updated dictionary.

This way, we already get a basic python structure, and we just have to populate it with real code. We can even keep most of the comments in the code so if somebody else tries to read your code, they will grasp the logic behind it.

For the first part, I’m reusing more or less the same code as my old function, just renaming a few things:

#Function to change tile_color:
def setTileColor(value = None):
    '''
    Changes the tile_color of multiple nodes at once
    '''
    if value == None:
        value = nuke.getColor()
    for node in nuke.selectedNodes():
        node.knob('tile_color').setValue(value)

For the second part and third, there is something new. We need to read and write values in a file. Although there are many ways to do that, in this case I will use a python module called JSON.
I picked JSON because that’s the one I’m most comfortable using, and if I open the file with a text editor, it looks just like a python variable. More info about JSON
The basic usage of JSON for us will be as follow:

# Reading a file
file = open(filePath)
data = json.load(file)
file.close()

# Writing a file
file = open(filePath, "w")
json.dump(data, file)
file.close()

Let’s look at the full code as a WIP

UGLY code

#Function to create the Menu:
def createTileColorMenu():
    print "creating menu"
    #We need a variable to define what should be in the menu
    #A dic would be perfect, with the menu name as a key, and the value as a value.
    colors = {}
    #We need to read this dic from a stored file somewhere
    import json, os
    path = os.path.expanduser("~/.nuke/nodeColorPresets.json")
    if not os.path.isfile(path):
        print "File does not exist"
    else:
        try:
            f = open(path)
            colors = json.load(f)
            f.close()
        except:
            pass

    #We need to call the menu to which we will add our "nodeColor" menu
    menu = nuke.toolbar("Nodes")
    #and create the menu.
    colorMenu = menu.addMenu( 'Color Nodes', icon="color_node.png")

    #for each key in the dictionary:
    for color in sorted(colors):
        #add a menu entry
        colorMenu.addCommand(color, lambda: setTileColor(colors[color]))

    #let's also add a "custom" menu entry (although nuke has one by default)
    colorMenu.addCommand("Custom Color", lambda: setTileColor())
    #and also a menu entry "Add Color Preset"
    colorMenu.addCommand("Add New Preset", lambda: addNewColor())
    #Let's make that one call another function.

#Function to add a color preset:
def addNewColor():
    #Open a color picker panel.
    color = nuke.getColor()
    #If the user doesn't click cancel:
    if color:
        #Open a Panel asking for a name.
        validName = False
        while not validName:
            name = nuke.getInput("Give a name to your color","color")
            #If the user doesn't click cancel:
            if name:
                #Verify the name isn't taken. Reopen the panel if it's already taken (loop)
                #Ohoh, we need the current dictionary to check if the name is taken..
                import json, os
                path = os.path.expanduser("~/.nuke/nodeColorPresets.json")
                if not os.path.isfile(path):
                    print "File does not exist"
                    colors = {}
                else:
                    try:
                        f = open(path)
                        print "File opened fine"
                        colors = json.load(f)
                        f.close()
                    except:
                        colors = {}

                #If everything ok:
                if not name in colors.keys():
                    print "OK"
                    validName = True
                    #add entry to the dictionary
                    colors[name] = color
                    f = open(path, "w")
                    json.dump(colors, f)
                    f.close()
                    #rebuild the menu with the updated dictionary.
                    createTileColorMenu()
                else:
                    nuke.message("The name already exists")
                #if no cancel was pressed, just stop the function
            else:
                return

This code is the result of trial and errors, it’s very messy because all that has been added has been added just to avoid errors.
The first time I run the code through, I got a few syntax errors. After going through, I wasn’t getting errors anymore, but the code wasn’t actually behaving the way I wanted.
I add little print statements thorough the code to see how far the code has run, or see which branch of an “if” test got run. It’s the most basic debugging.
The second part of debugging to me is to think of “What could go wrong? How could a user screw up and use the tool wrong?”
Adding a few “try:’ to catch errors and fallback on something controllable is handy.

The next step is to clean that up. I would usually try to clean up as I code but for the demonstration I was trying to respect my initial layout.

There are parts of the code that I repeat (Like the reading of the file), so I can do a bit of shuffling around and make it a function to make the code clearer and more efficient.
It’s probably a good idea to do all the imports at the beginning, and to import nuke and nukescripts as well.

Final Code: (for Part 1)

import nuke, nukescripts
import json, os

# Settings
path = os.path.expanduser("~/.nuke/nodeColorPresets.json")
targetMenu = nuke.toolbar("Nodes")

# Function to read the user presets.
def readColorPresets(path):
    if not os.path.isfile(path):
        print "No Color preset found "
        return {}
    else:
        try:
            f = open(path)
            colors = json.load(f)
            f.close()
            if type(colors) is dict:
                return colors
            else:
                print "The preset file doesn't contain a valid dictionary"
                return {}
        except:
            print "Error reading color preset file"
            return {}

# Function to write the user presets.
def writeColorPresets(path, colors):
    try:
        f = open(path, "w")
        json.dump(colors, f)
        f.close()
        return True
    except:
        return False

#Function to change tile_color:
def setTileColor(value = None):
    '''
    Changes the tile_color of multiple nodes at once
    '''
    if value == None:
        value = nuke.getColor()
    for node in nuke.selectedNodes():
        node.knob('tile_color').setValue(value)

#Function to create the Menu:
def createTileColorMenu():
    # Loading out color presets
    colors = readColorPresets(path)

    # And creating the menu.
    colorMenu = targetMenu.addMenu( 'Color Nodes', icon="color_node.png")

    # Populating the Menu
    for color in sorted(colors):
        colorMenu.addCommand(color, lambda: setTileColor(colors[color]))

    colorMenu.addCommand("Custom Color", lambda: setTileColor())
    colorMenu.addCommand("Add New Preset", lambda: addNewColor())

#Function to add a color preset:
def addNewColor():
    #Open a color picker panel.
    color = nuke.getColor()
    if color:
        #Load our colors from the file
        colors = readColorPresets(path)
        validName = False
        while not validName:
            name = nuke.getInput("Give a name to your color","color")
            if name:
                #Verify the name isn't taken. Reopen the panel if it's already taken
                if not name in colors.keys():
                    validName = True # Get out of the loop
                    #add entry to the dictionary
                    colors[name] = color
                    success = writeColorPresets(path, colors)
                    if success:
                        targetMenu.findItem("Color Nodes").clearMenu()
                        createTileColorMenu()
                    else:
                        print "Error writing file %s" % path
                else:
                    nuke.message("The name already exists")

createTileColorMenu()

Edit: I replaced lambda: setTileColor(colors

[color]) line 57 by ‘setTileColor(%s)’% colors[color] The reason was that the lambda does not get evaluated until you click on the menu, which means you could only set the color to the last color added to the menu… not ideal. With this, the color value get evaluated at the menu creation, and the menu item has a hard coded value to it.

Notice how much easier to read it gets once cleaned up, even with much less comments.
I added a few more checks (like making sure the variable we get out of the file is a dictionary), and gathering a few variables at the top can be useful for variables that may need customization.

At this point we have a very usable plugin. You could save that as colorNodesPresets.py somewhere is your nuke plugin path, and add “import colorNodesPresets” to your menu.py.
You could also add an icon called color_node.png somewhere in your nuke plugin path and that would load in as your menu icon.

In the next part of the “tutorial” I will try to update that same plugin, and add a way to manage/delete presets.
And just for the challenge, I’ll try to write a function that could name a color automatically.

Stay posted.