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 the 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) I’d like to be able to automatically prefill 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 its 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 dict would be perfect, with the menu name as a key, and the value as a value.
    # We need to read this dict 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 is 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 quite like a python dictionary. 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()

Edit 2021: The recommended pattern for opening and closing files in python is to use a context. It’s a bit out of scope for this tutorial, but in short the 2 examples above would now look like this:

# Reading a file
with open(filePath) as my_file:
    data = json.load(my_file)

# Writing a file
with open(filePath, "w") as my_file:
    json.dump(data, my_file)

This is a bit more concise, and you don’t need to worry about closing the file, as Python will close it automatically when it reaches the end of the indented block, even if something goes wrong and errors. End of Edit.

Let’s look at the added 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 dict 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 x=colors[color]: setTileColor(x))

    # 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:
                    print("OK")
                    validName = True  # We need to set this so the while loop stops executing
                    # 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 cancel was pressed, just stop the function
            else:
                return

This code is the result of trial and error, it’s a bit messy because of all that has been added just to avoid errors.
The first time I run the code though, 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 had run or see which branch of an “if” test ran. 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 as well.

Final Code: (for Part 1)

import os
import json
import nuke

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


# Function to read the user presets.
def readColorPresets(path):
    """
    Read Color presets from Json file
    """
    if not os.path.isfile(path):
        print("No Color preset found")
        return {}
    else:
        try:
            with open(path) af f:
                colors = json.load(f)
            return colors
        except:
            print("Error reading color preset file")
            return {}


def writeColorPresets(path, colors):
    """
    Write Color presets to Json file
    """
    try:
        with open(path, "w") as f:
            json.dump(colors, f)
        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 is None:
        value = nuke.getColor()
    for node in nuke.selectedNodes():
        node.knob('tile_color').setValue(value)


def createTileColorMenu():
    """
    Create the Menu entry for the Tile Color Presets
    """
    # Loading out color presets
    colors = readColorPresets(PATH)

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

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

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


def addNewColor():
    """
    Add a new color preset to our presets.
    """
    # Open a color picker panel.
    color = nuke.getColor()
    if color:
        #Load our colors from the file
        colors = readColorPresets(PATH)
        valid_name = False
        while not valid_name:
            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:
                    valid_name = True  # Get out of the loop
                    #add entry to the dictionary
                    colors[name] = color
                    success = writeColorPresets(PATH, colors)
                    if success:
                        TARGET_MENU.findItem("Color Nodes").clearMenu()
                        createTileColorMenu()
                    else:
                        print("Error writing file {}".format(PATH)
                else:
                    nuke.message("The name already exists")

createTileColorMenu()

Notice how much easier to read it gets once cleaned up, even with much fewer 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 in 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.

Go to Part 2