Tutorial: Modifying shortcuts with Python in Silhouette

This is a min-tutorial on customizing Silhouette shortcuts with Python.

The great thing about the Silhouette key binding system is it gives you a lot of flexibility that would otherwise be difficult from a standard keyboard shortcut setup.

Thanks to Katie Morris for the request to have this script.

Thanks to Paul Miller for writing the Python script presented here, that cleaned up and improved my original approach.

In this example, we’re going to augment the preset shortcuts for the Paint node brushes so you can quickly cycle through the presets using the TAB key.

Knowledge of Python does help, but you should still be able to follow along.

First of all, you’re going to need to open the keybinds.py script found at these default locations:

Linux
/opt/SilhouetteFX/silhouette [version]/resources
OS X
/Applications/SilhouetteFX/Silhouette [version]/Silhouette.app/Contents/Resources
Windows
C:\Program Files\SilhouetteFX\Silhouette [version]\resources

You can modify where these scripts are located by using the Silhouette environment variables, but that’s a tutorial for a different day.

DO THIS FIRST: The keybinds.py file may be in a directory that is read-only. Make sure you check your write permissions first, or save to a separate location and copy over the file at the end.

DO THIS NEXT: Make a backup of your keybinds.py script. Call this file keybinds.py.bak or .orig or whatever you like your backups to be called. I won’t judge you.

Once you have opened a new copy of keybinds.py, do a search for setPaintPreset to find where the initial shortcuts for the Paint Presets are set up:

def setPaintPreset(index):
	fx.paint.preset = index

fx.bind("Alt+1", callMethod(setPaintPreset, 0))
fx.bind("Alt+2", callMethod(setPaintPreset, 1))
fx.bind("Alt+3", callMethod(setPaintPreset, 2))
fx.bind("Alt+4", callMethod(setPaintPreset, 3))
fx.bind("Alt+5", callMethod(setPaintPreset, 4))
fx.bind("Alt+6", callMethod(setPaintPreset, 5))
fx.bind("Alt+7", callMethod(setPaintPreset, 6))
fx.bind("Alt+8", callMethod(setPaintPreset, 7))
fx.bind("Alt+9", callMethod(setPaintPreset, 8))
fx.bind("Alt+0", callMethod(setPaintPreset, 9))

We’re not going to touch this code. Instead, we’re going to create some new code underneath based on the same principles.

We’re first going to check if we’re actually inside a Paint node by writing a small method that returns the current paint node or just ”None” if it isn’t a paint node:

def getPaintNode():
	node = fx.activeNode()	#gets the currently active node
	if node != None and node.isType("PaintNode"):
		return node #if the active node is a paint node, return it
	return None #otherwise return the value None

This helps us avoid problems with other shortcuts that may be set to the same key.

Let’s now define the method that will handle cycling between the brush presets using the tab key. We do this with a Python function:

def cyclePaintPreset(direction=1):

The “direction” variable will determine which way the cycling occurs.

NOTE: From here on out I will be reposting the fully written code several times from the top of the method so you can see the full flow.

Inside cyclePaintPreset we can now run that check to see if we’re using a paint node:

def cyclePaintPreset(direction=1):
	node = getPaintNode() 
	if not node:
		return # if we don’t have a Paint node, get out of there!

Next, we want to get the number of presets we want to cycle through. Since there are 10, we can assign up to that value, but you may want less if you have a ‘Top 4’ that you constantly switch between:

def cyclePaintPreset(direction=1):
	node = getPaintNode()
	if not node:
		return

	num_presets = 10 #change this value to the number of presets you want to cycle through

We then need to assign the current preset and an index value so we can know where we’re up to:

def cyclePaintPreset(direction=1):
	node = getPaintNode()
	if not node:
		return

	num_presets = 10
	current = fx.paint.preset #grab the currently assigned preset
	if current < 0:
		return #if there are no presets, don’t continue
	index = current # assign the current preset as our index

Now we start the main loop so we can cycle around the presets:

def cyclePaintPreset(direction=1):
	node = getPaintNode()
	if not node:
		return

	num_presets = 10
	current = fx.paint.preset
	if current < 0:
		return	
	index = current
	while True:
		index = index + direction
		# handle wraparound
		if index < 0:
			index = num_presets - 1
		elif index >= num_presets:
			index = 0

In the above code the direction value is added to the index. Later in the code we will assign -1 to the opposite direction when we hold down SHIFT.

The if statements check to see if the presets have hit their limit in either direction and then reset them to the next wrapped value. This avoids the problem of incrementing or decrementing too far in either direction.

If there is only one preset, we want to avoid looping and get out:

def cyclePaintPreset(direction=1):
	node = getPaintNode()
	if not node:
		return

	num_presets = 10
	current = fx.paint.preset
	if current < 0:
		return	
	index = current
	while True:
		index = index + direction
		# handle wraparound
		if index < 0:
			index = num_presets - 1
		elif index >= num_presets:
			index = 0

		# avoid infinite loop if only one preset
		if index == current:
			break

Finally, we want to make sure there is actually a preset set for the brush at the value we’re cycling to, so we need to check if the node state contains anything:

def cyclePaintPreset(direction=1):
	node = getPaintNode()
	if not node:
		return

	num_presets = 10
	current = fx.paint.preset
	if current < 0:
		return	
	index = current
	while True:
		index = index + direction
		# handle wraparound
		if index < 0:
			index = num_presets - 1
		elif index >= num_presets:
			index = 0

		# avoid infinite loop if only one preset
		if index == current:
			break

		# check for a preset
		try:
			preset = node.state["preset%d" % (index)]
			fx.paint.preset = index
			return
		except:
			pass

That’s all you need for the method, so now we need to bind it to the relevant keys. This is as simple as using the fx.bind function to assign “Tab” to the method we just created:

fx.bind("Tab", callMethod(cyclePaintPreset, direction=1))

Note that in the above call, we add “direction=1” to tell the cyclePaintPreset method which way we want to go for that keypress.

To go in the reverse direction, we just need to call the method again and assign it to a different keyboard shortcut. In this case, we’ll use “Shift+Tab”:

fx.bind("Shift+Tab", callMethod(cyclePaintPreset, direction=-1))

Now you can save your keybinds.py script and reboot Silhouette.

Now when you use a Paint node, you can still use the Alt+number shortcuts, or use the Tab key to cycle between the saved presets.

The final script looks like this:

def getPaintNode():
	node = fx.activeNode()	
	if node != None and node.isType("PaintNode"):
		return node
	return None

def cyclePaintPreset(direction=1):
	node = getPaintNode()
	if not node:
		return

	num_presets = 10
	current = fx.paint.preset
	if current < 0:
		return	
	index = current
	while True:
		index = index + direction
		# handle wraparound
		if index < 0:
			index = num_presets - 1
		elif index >= num_presets:
			index = 0

		# avoid infinite loop if only one preset
		if index == current:
			break

		# check for a preset
		try:
			preset = node.state["preset%d" % (index)]
			fx.paint.preset = index
			return
		except:
			pass

fx.bind("Tab", callMethod(cyclePaintPreset, direction=1))
fx.bind("Shift+Tab", callMethod(cyclePaintPreset, direction=-1))
2 Likes

This is great - thank you Martin and Paul!

Awesome! Great post & idea. May have to add this to my current paint presets setup! I have a keybind that automatically sets all the presets to a custom defined setup with certain settings I always like so you don’t have to manually create the same presets each time on launch. Here’s a snippet.

    > def paint_presets():
    	node = fx.activeNode()
    	if node.type == 'PaintNode':

                # Preset 01
		fx.paint.setState('Clone.port:0', 'output')
		fx.paint.setState('Clone.frame:0', 0)
		fx.paint.savePreset(0)

		# Preset 02
		fx.paint.setState('Clone.port:0', 'clone1')
		fx.paint.setState('Clone.frame:0', 0)
		fx.paint.savePreset(1)

		# Preset 03
		fx.paint.setState('Clone.port:0', 'output')
		fx.paint.setState('Clone.frame:0', -1)
		fx.paint.savePreset(2)

		# Preset 04
		fx.paint.setState('Clone.port:0', 'output')
		fx.paint.setState('Clone.frame:0', +1)
                fx.paint.savePreset(3)

                # Preset 05
	        fx.viewer.selectTool('Drag')
	        fx.paint.setState('opacity', 50)
	        fx.paint.setState('spacing', 5)
	        fx.paint.savePreset(4)

    if fx.gui:
        fx.bind('F10', paint_presets)

And so on etc for all the presets. (Apologies if that is formatted incorrectly). The tab scroll is really neat though! Thanks

1 Like

@joshbarham you could also register a object_added hook where you can check for a new PaintNode. Then you don’t need to use up a bind and remember to press the key.

More info on hooks at the Silhouette Scripting wiki: https://documentation.borisfx.com/wiki/sfx/index.php?title=Scripting_Guide#Hooks

1 Like