#------------------------------------------------------
# Stamps by Adrian Pueyo and Alexey Kuchinski
# Smart node connection system for Nuke
# adrianpueyo.com, 2018-2019
version= "v1.0"
date = "Sep 27 2019"
#-----------------------------------------------------
# Constants
STAMP_DEFAULTS = { "note_font_size":20, "hide_input":0 }
ANCHOR_DEFAULTS = { "tile_color" : int('%02x%02x%02x%02x' % (255,255,255,1),16),
"autolabel": 'nuke.thisNode().knob("title").value()',
"knobChanged":'stamps.anchorKnobChanged()',
"onCreate":'if nuke.GUI:\n try:\n import stamps; stamps.anchorOnCreate()\n except:\n pass'}
WIRED_DEFAULTS = { "tile_color" : int('%02x%02x%02x%02x' % (1,0,0,1),16),
"autolabel": 'nuke.thisNode().knob("title").value()',
"knobChanged":'import stamps; stamps.wiredKnobChanged()'}
DeepExceptionClasses = ["DeepToImage","DeepHoldout","DeepHoldout2"] # Nodes with "Deep" in their class that don't classify as Deep.
NodeExceptionClasses = ["Viewer"] # Nodes that won't accept stamps
ParticleExceptionClasses = ["ParticleToImage"] # Nodes with "Particle" in class and an input called "particles" that don't classify as particles.
StampClasses = {"2D":"NoOp", "Deep":"DeepExpression"}
AnchorClassesAlt = {"2D":"NoOp", "Deep":"DeepExpression"}
StampClassesAlt = {"2D":"NoOp", "Deep":"DeepExpression", "3D":"LookupGeo", "Camera":"DummyCam", "Axis":"Axis", "Particle":"ParticleExpression"}
InputIgnoreClasses = ["NoOp", "Dot", "Reformat", "DeepReformat", "Crop"]
TitleIgnoreClasses = ["NoOp", "Dot", "Reformat", "DeepReformat", "Crop"]
TagsIgnoreClasses = ["NoOp", "Dot", "Reformat", "DeepReformat", "Crop"]
AnchorClassColors = {"Camera":int('%02x%02x%02x%02x' % (255,255,255,1),16),}
WiredClassColors = {"Camera":int('%02x%02x%02x%02x' % (51,0,0,1),16),}
STAMPS_HELP = "Stamps by Adrian Pueyo and Alexey Kuchinski.\nUpdated "+date
VERSION_TOOLTIP = "Stamps by Adrian Pueyo and Alexey Kuchinski.\nUpdated "+date+"."
STAMPS_SHORTCUT = "F8"
KEEP_ORIGINAL_TAGS = True
if not globals().has_key('Stamps_LastCreated'):
Stamps_LastCreated = None
if not globals().has_key('Stamps_MenusLoaded'):
Stamps_MenusLoaded = False
Stamps_LockCallbacks = False
import nuke
import nukescripts
import re
from functools import partial
# PySide import switch
try:
if nuke.NUKE_VERSION_MAJOR < 11:
from PySide import QtCore, QtGui, QtGui as QtWidgets
from PySide.QtCore import Qt
else:
from PySide2 import QtWidgets, QtGui, QtCore
from PySide2.QtCore import Qt
except ImportError:
from Qt import QtCore, QtGui, QtWidgets
# Import stamps_config
# Optional: place the stamps_config.py file anywhere in your python path (i.e. in your /.nuke folder)
anchor_defaults = STAMP_DEFAULTS.copy()
anchor_defaults.update(ANCHOR_DEFAULTS)
wired_defaults = STAMP_DEFAULTS.copy()
wired_defaults.update(WIRED_DEFAULTS)
try:
from stamps_config import *
if ANCHOR_STYLE:
anchor_defaults.update(ANCHOR_STYLE)
if STAMP_STYLE:
wired_defaults.update(STAMP_STYLE)
except:
pass
#################################
### FUNCTIONS INSIDE OF BUTTONS
#################################
def wiredShowAnchor():
n = nuke.thisNode()
a_name = n.knob("anchor").value()
if nuke.exists(a_name):
nuke.show(nuke.toNode(a_name))
elif n.inputs():
nuke.show(n.input(0))
def wiredZoomAnchor():
n = nuke.thisNode()
a_name = n.knob("anchor").value()
if nuke.exists(a_name):
a = nuke.toNode(a_name)
#nuke.show(a)
nuke.zoom(nuke.zoom(),[a.xpos()+a.screenWidth()/2,a.ypos()+a.screenHeight()/2])
elif n.inputs():
ni = n.input(0)
nuke.zoom(nuke.zoom(),[ni.xpos()+ni.screenWidth()/2,ni.ypos()+ni.screenHeight()/2])
def wiredZoomThis():
n = nuke.thisNode()
nuke.zoom(nuke.zoom(),[n.xpos(),n.ypos()])
def wiredStyle(n, style = 0):
''' Change the style of a wired stamp, based on some presets '''
if "note_font_size" in wired_defaults.keys():
size = wired_defaults["note_font_size"]
else:
size = 20
nf = n["note_font"].value().split(" Bold")[0].split(" bold")[0]
if style == 0: # DEFAULT
n["note_font_size"].setValue(size)
n["note_font_color"].setValue(0)
n["note_font"].setValue(nf)
elif style == 1: # BROKEN
n["note_font_size"].setValue(size*2)
n["note_font_color"].setValue(4278190335)
n["note_font"].setValue(nf+" Bold")
def wiredGetStyle(n):
''' Check connection status of wired and set the style accordingly. '''
if not isWired(n):
return False
if not n.inputs():
wiredStyle(n,1)
elif not isAnchor(n.input(0)):
wiredStyle(n,1)
elif n["anchor"].value() != n.input(0).name():
wiredStyle(n,1)
else:
wiredStyle(n,0)
def wiredTagsAndBackdrops(n, updateSimilar=False):
try:
a = n.input(0)
if not a:
return
a_tags = a["tags"].value().strip().strip(",")
a_bd = backdropTags(a)
an = n.knob("anchor").value()
if updateSimilar:
ns = [i for i in allWireds() if i.knob("anchor").value() == an]
else:
ns = [n]
for n in ns:
try:
tags_knob = n.knob("tags")
bd_knob = n.knob("backdrops")
[i.setVisible(False) for i in [tags_knob, bd_knob]]
if a_tags:
tags_knob.setValue("{}".format(a_tags))
tags_knob.setVisible(True)
if len(a_bd) and a_bd != []:
bd_knob.setValue("{}".format(",".join(a_bd)))
bd_knob.setVisible(True)
except:
pass
except:
try:
[i.setVisible(False) for i in [tags_knob, bd_knob]]
except:
pass
def wiredKnobChanged():
global Stamps_LockCallbacks
k = nuke.thisKnob()
kn = k.name()
if kn in ["xpos","ypos","reconnect_by_selection_this","reconnect_by_selection_similar"]:
return
n = nuke.thisNode()
if Stamps_LockCallbacks == True:
return
ni = n.inputs()
if n.knob("toReconnect") and n.knob("toReconnect").value() and nuke.GUI:
if not ni:
if n.knob("auto_reconnect_by_title") and n.knob("auto_reconnect_by_title").value() and n.knob("title"):
n.knob("auto_reconnect_by_title").setValue(0)
for a in allAnchors():
if a.knob("title") and a["title"].value() == n["title"].value():
n.knob("auto_reconnect_by_title").setValue(False)
nuke.thisNode().setInput(0,a)
n["anchor"].setValue(a.name())
wiredStyle(n)
return
try:
inp = n.knob("anchor").value()
a = nuke.toNode(inp)
if a.knob("title") and n.knob("title") and a["title"].value() == n["title"].value():
nuke.thisNode().setInput(0,a)
wiredStyle(n)
else:
wiredStyle(n,1)
except:
wiredGetStyle(n)
else:
try:
a = n.input(0)
if isAnchor(a):
if a.knob("title") and n.knob("title") and a["title"].value() == n["title"].value():
n.knob("anchor").setValue(a.name())
else:
inp = n.knob("anchor").value()
a = nuke.toNode(inp)
if a.knob("title") and n.knob("title") and a["title"].value() == n["title"].value():
nuke.thisNode().setInput(0,a)
else:
wiredStyle(n,1)
n.setInput(0,None)
except:
pass
n.knob("toReconnect").setValue(False)
elif not ni:
if nodeType(n)=="Particle" and not nuke.env["nukex"]:
return
wiredStyle(n,1)
return
elif kn == "selected": #First time it's this knob, it will activate the first if, then, ignore.
return
elif kn == "inputChange":
wiredGetStyle(n)
elif kn == "title":
kv = k.value()
if titleIsLegal(kv):
if nuke.ask("Do you want to update the linked stamps' title?"):
a = retitleAnchor(n) # Retitle anchor
retitleWired(a) # Retitle wired stamps of anchor a
return
else:
nuke.message("Please set a valid title.")
try:
n["title"].setValue(n["prev_title"].value())
except:
pass
else:
try:
n.knob("toReconnect").setValue(False)
if ni:
if isAnchor(n.input(0)):
if n.knob("title").value() == n.input(0).knob("title").value():
n.knob("anchor").setValue(n.input(0).name())
elif nuke.ask("Do you want to change the anchor for the current stamp?"):
n.knob("anchor").setValue(n.input(0).name())
n.knob("title").setValue(n.input(0).knob("title").value())
n.knob("prev_title").setValue(n.input(0).knob("title").value())
else:
n.setInput(0,None)
try:
n.setInput(0,nuke.toNode(n.knob("anchor").value()))
except:
pass
wiredGetStyle(n)
except:
pass
if kn == "showPanel":
wiredTagsAndBackdrops(n)
def wiredOnCreate():
n = nuke.thisNode()
n.knob("toReconnect").setValue(1)
for k in n.allKnobs():
if k.name() not in ['wired_tab','identifier','lockCallbacks','toReconnect','title','prev_title','tags','backdrops','anchor','line1','anchor_label','show_anchor','zoom_anchor','stamps_label','zoomNext','selectSimilar','space_1','reconnect_label','reconnect_this','reconnect_similar','reconnect_all','space_2','advanced_reconnection','reconnect_by_title_label','reconnect_by_title_this','reconnect_by_title_similar', 'reconnect_by_title_selected', 'reconnect_by_selection_label','reconnect_by_selection_this','reconnect_by_selection_similar','reconnect_by_selection_selected','auto_reconnect_by_title','advanced_reconnection','line2','buttonHelp','version']:
k.setFlag(0x0000000000000400)
def anchorKnobChanged():
k = nuke.thisKnob()
kn = k.name()
if kn in ["xpos","ypos"]:
return
n = nuke.thisNode()
if kn == "title":
kv = k.value()
if titleIsLegal(kv):
if nuke.ask("Do you want to update the linked stamps' title?"):
retitleWired(n) # Retitle wired stamps of anchor a
return
else:
nuke.message("Please set a valid title.")
try:
n["title"].setValue(n["prev_title"].value())
except:
pass
elif kn == "name":
try:
nn = anchor["prev_name"].value()
except:
nn = anchor.name()
children = anchorWireds(n)
for i in children:
i.knob("anchor").setValue(nn)
anchor["prev_name"].setValue(anchor.name())
elif kn == "tags":
for ni in allWireds():
if ni.knob("anchor").value() == n.name():
wiredTagsAndBackdrops(ni, updateSimilar=True)
return
def anchorOnCreate():
n = nuke.thisNode()
for k in n.allKnobs():
if k.name() not in ['anchor_tab','identifier','title','prev_title','prev_name','showing','tags','stamps_label','selectStamps','reconnectStamps','zoomNext','createStamp','buttonHelp','line1','line2','version']:
k.setFlag(0x0000000000000400)
try:
nn = n["prev_name"].value()
except:
nn = n.name()
children = anchorWireds(n)
for i in children:
i.knob("anchor").setValue(nn)
n["prev_name"].setValue(n.name())
return
def retitleAnchor(ref = ""):
'''
Retitle Anchor of current wired stamp to match its title.
returns: anchor node
'''
if ref == "":
ref = nuke.thisNode()
try:
ref_title = ref["title"].value()
if ref_title.strip() != "":
ref_title = ref_title.strip()
ref_anchor = ref["anchor"].value()
na = nuke.toNode(ref_anchor)
for kn in ["title","prev_title"]:
na[kn].setValue(ref_title)
ref["prev_title"].setValue(ref_title)
return na
except:
return None
def retitleWired(anchor = ""):
'''
Retitle wired stamps connected to supplied anchor
'''
if anchor == "":
return
try:
anchor_title = anchor["title"].value()
anchor_name = anchor.name()
for nw in allWireds():
if nw["anchor"].value() == anchor_name:
nw["title"].setValue(anchor_title)
nw["prev_title"].setValue(anchor_title)
return True
except:
pass
def wiredSelectSimilar(anchor_name = ""):
if anchor_name=="":
anchor_name = nuke.thisNode().knob("anchor").value()
for i in allWireds():
if i.knob("anchor").value() == anchor_name:
i.setSelected(True)
def wiredReconnect(n=""):
succeeded=True
if n=="":
n = nuke.thisNode()
try:
anchor = nuke.toNode(n.knob("anchor").value())
if not anchor:
succeeded = False
n.setInput(0,anchor)
except:
succeeded = False
try:
wiredGetStyle(n)
except:
pass
return succeeded
def wiredReconnectSimilar(anchor_name = ""):
if anchor_name=="":
anchor_name = nuke.thisNode().knob("anchor").value()
for i in nuke.allNodes():
if isWired(i) and i.knob("anchor").value() == anchor_name:
reconnectErrors = 0
try:
i.knob("reconnect_this").execute()
except:
reconnectErrors += 1
finally:
if reconnectErrors > 0:
nuke.message("Couldn't reconnect {} nodes".format(str(reconnectErrors)))
wiredGetStyle(i)
def wiredReconnectAll():
for i in nuke.allNodes():
if isWired(i):
reconnectErrors = 0
try:
i.knob("reconnect_this").execute()
except:
reconnectErrors += 1
finally:
if reconnectErrors > 0:
nuke.message("Couldn't reconnect {} nodes".format(str(reconnectErrors)))
def wiredReconnectByTitle(title=""):
#1. Find matching nodes.
n = nuke.thisNode()
if title=="":
title = n.knob("title").value()
matches = []
for i in nuke.allNodes():
if isAnchor(i) and i.knob("title").value() == title:
matches.append(i)
#2. Do what's necessary
num_matches = len(matches)
if num_matches == 1: # One match -> Connect
anchor = matches[0]
n["anchor"].setValue(anchor.name())
n.setInput(0,anchor)
elif num_matches > 1:
# More than one match...
ns = nuke.selectedNodes()
if ns and len(ns) == 1 and isAnchor(ns[0]):
# Exactly one anchor selected and title matches -> connect
if ns[0].knob("title").value() == title:
n["anchor"].setValue(ns[0].name())
n.setInput(0,ns[0])
n.knob("reconnect_this").execute()
else:
# Selection not matching -> Message asking to select a specific anchor.
nuke.message("More than one Anchor Stamp found with the same title. Please select the one you like in the Node Graph and click this button again.")
elif num_matches == 0:
nuke.message("No Anchor Stamps with title '{}' found in the script.".format(title))
def wiredReconnectByTitleSimilar(title=""):
#1. Find matching anchors.
n = nuke.thisNode()
if title=="":
title = n.knob("title").value()
matches = []
for i in allAnchors():
if i.knob("title").value() == title:
matches.append(i)
#2. Do what's necessary
num_matches = len(matches)
if num_matches == 0:
# No matches -> abort
nuke.message("No Anchor Stamps with title '{}' found in the script.")
return
anchor_name = n.knob("anchor").value()
siblings = [i for i in nuke.allNodes() if isWired(i) and i.knob("anchor").value() == anchor_name]
if num_matches == 1: # One match -> Connect
anchor = matches[0]
for s in siblings:
s["anchor"].setValue(anchor.name())
s.setInput(0,anchor)
wiredStyle(s,0)
s.knob("reconnect_this").execute()
elif num_matches > 1:
# More than one match...
ns = nuke.selectedNodes()
if ns and len(ns) == 1 and isAnchor(ns[0]):
# Exactly one anchor selected and title matches -> connect
if ns[0].knob("title").value() == title:
for s in siblings:
s["anchor"].setValue(ns[0].name())
s.setInput(0,ns[0])
wiredStyle(s,0)
s.knob("reconnect_this").execute()
else:
# Selection not matching -> Message asking to select a specific anchor.
nuke.message("More than one Anchor Stamp found with the same title. Please select the one you like in the Node Graph and click this button again.")
def wiredReconnectByTitleSelected():
#1. Find matching anchors.
ns = nuke.selectedNodes()
ns = [i for i in ns if isWired(i)]
for n in ns:
title = n.knob("title").value()
matches = []
for i in allAnchors():
if i.knob("title").value() == title:
matches.append(i)
#2. Do what's necessary only for the one match cases
anchor_name = n.knob("anchor").value()
if len(matches) == 1: # One match -> Connect
anchor = matches[0]
n["anchor"].setValue(anchor.name())
n.setInput(0,anchor)
wiredStyle(n,0)
n.knob("reconnect_this").execute()
def wiredReconnectBySelection():
global Stamps_LockCallbacks
n = nuke.thisNode()
ns = nuke.selectedNodes()
if not len(ns):
nuke.message("Please select an Anchor Stamp first.")
elif len(ns)>1:
nuke.message("Multiple nodes selected, please select only one Anchor Stamp.")
else:
if not isAnchor(ns[0]):
nuke.message("Please select an Anchor Stamp.")
else:
Stamps_LockCallbacks = True
n["anchor"].setValue(ns[0].name())
n["title"].setValue(ns[0]["title"].value())
n.setInput(0,ns[0])
wiredGetStyle(n)
Stamps_LockCallbacks = False
n.knob("reconnect_this").execute()
def wiredReconnectBySelectionSimilar():
global Stamps_LockCallbacks
n = nuke.thisNode()
ns = nuke.selectedNodes()
if not len(ns):
nuke.message("Please select an Anchor Stamp first.")
elif len(ns)>1:
nuke.message("Multiple nodes selected, please select only one Anchor Stamp.")
elif not isAnchor(ns[0]):
nuke.message("Please select an Anchor Stamp.")
else:
anchor_name = n.knob("anchor").value()
siblings = [i for i in nuke.allNodes() if isWired(i) and i.knob("anchor").value() == anchor_name]
for s in siblings:
Stamps_LockCallbacks = True
s["anchor"].setValue(ns[0].name())
s["title"].setValue(ns[0]["title"].value())
s.setInput(0,ns[0])
wiredStyle(s,0)
Stamps_LockCallbacks = False
s.knob("reconnect_this").execute()
def wiredReconnectBySelectionSelected():
global Stamps_LockCallbacks
n = nuke.thisNode()
ns = nuke.selectedNodes()
if not len(ns):
nuke.message("Please select one Anchor plus one or more Stamps first.")
return
anchors = []
stamps = []
for i in ns:
if isAnchor(i):
anchors.append(i)
if isWired(i):
stamps.append(i)
if len(anchors) != 1:
nuke.message("Please select one Anchor, plus one or more Stamps.")
return
else:
anchor = anchors[0]
if not len(stamps):
nuke.message("Please, also select one or more Stamps that you want to reconnect to the selected Anchor.")
for s in stamps:
Stamps_LockCallbacks = True
s["anchor"].setValue(anchor.name())
s["title"].setValue(anchor["title"].value())
s.setInput(0,anchor)
wiredStyle(s,0)
Stamps_LockCallbacks = False
s.knob("reconnect_this").execute()
def anchorReconnectWired(anchor = ""):
if anchor=="":
anchor = nuke.thisNode()
anchor_name = anchor.name()
for i in allWireds():
if i.knob("anchor").value() == anchor_name:
reconnectErrors = 0
try:
i.setInput(0,anchor)
except:
reconnectErrors += 1
finally:
if reconnectErrors > 0:
nuke.message("Couldn't reconnect {} nodes".format(str(reconnectErrors)))
def wiredZoomNext(anchor_name = ""):
if anchor_name=="":
anchor_name = nuke.thisNode().knob("anchor").value()
anchor = nuke.toNode(anchor_name)
showing_knob = anchor.knob("showing")
showing_value = showing_knob.value()
i = 0
for ni in allWireds():
if ni.knob("anchor").value() == anchor_name:
if i == showing_value:
nuke.zoom(1.5,[ni.xpos()+ni.screenWidth()/2,ni.ypos()+ni.screenHeight()/2])
showing_knob.setValue(i+1)
return
i+=1
showing_knob.setValue(0)
nuke.message("Couldn't find any more similar wired stamps.")
def anchorSelectWireds(anchor = ""):
if anchor == "":
try:
anchor = nuke.selectedNode()
except:
pass
if isAnchor(anchor):
anchor.setSelected(False)
wiredSelectSimilar(anchor.name())
def anchorWireds(anchor = ""):
''' Returns a list of the children stamps to the anchor with the specified name '''
if anchor == "":
try:
anchor = nuke.selectedNode()
except:
pass
if isAnchor(anchor):
try:
nn = anchor["prev_name"].value()
except:
nn = anchor.name()
children = [i for i in allWireds() if i.knob("anchor").value() == nn]
return children
wiredOnCreate_code = """if nuke.GUI:
try:
import stamps; stamps.wiredOnCreate()
except:
pass
"""
wiredReconnectToTitle_code = """n = nuke.thisNode()
try:
nt = n.knob("title").value()
for a in nuke.allNodes():
if a.knob("identifier").value() == "anchor" and a.knob("title").value() == nt:
n.setInput(0,a)
break
except:
nuke.message("Unable to reconnect.")
"""
wiredReconnect_code = """n = nuke.thisNode()
try:
n.setInput(0,nuke.toNode(n.knob("anchor").value()))
except:
nuke.message("Unable to reconnect.")
try:
import stamps
stamps.wiredGetStyle(n)
except:
pass
"""
#################################
### STAMP, ANCHOR, WIRED
#################################
def anchor(title = "", tags = "", input_node = "", node_type = "2D"):
''' Anchor Stamp '''
try:
n = nuke.createNode(AnchorClassesAlt[node_type])
except:
try:
n = nuke.createNode(StampClasses[node_type])
except:
n = nuke.createNode("NoOp")
name = getAvailableName("Anchor",rand=True)
n["name"].setValue(name)
# Set default knob values
for i,j in anchor_defaults.items():
try:
n.knob(i).setValue(j)
except:
pass
if node_type in AnchorClassColors:
try:
n["tile_color"].setValue(AnchorClassColors[node_type])
except:
pass
for k in n.allKnobs():
k.setFlag(0x0000000000000400)
# Main knobs
anchorTab_knob = nuke.Tab_Knob('anchor_tab','Anchor Stamp')
identifier_knob = nuke.Text_Knob('identifier','identifier', 'anchor')
identifier_knob.setVisible(False)
title_knob = nuke.String_Knob('title','Title:', title)
title_knob.setTooltip("Displayed name on the Node Graph for this Stamp and its Anchor.\nIMPORTANT: This is only for display purposes, and is different from the real/internal name of the Stamps.")
prev_title_knob = nuke.Text_Knob('prev_title','', title)
prev_title_knob.setVisible(False)
prev_name_knob = nuke.Text_Knob('prev_name','', name)
prev_name_knob.setVisible(False)
showing_knob = nuke.Int_Knob('showing','', 0)
showing_knob.setVisible(False)
tags_knob = nuke.String_Knob('tags','Tags', tags)
tags_knob.setTooltip("Comma-separated tags you can define for each Anchor, that will help you find it when invoking the Stamp Selector by pressing the Stamps shortkey with nothing selected.")
for k in [anchorTab_knob, identifier_knob, title_knob, prev_title_knob, prev_name_knob, showing_knob, tags_knob]:
n.addKnob(k)
n.addKnob(nuke.Text_Knob("line1", "", "")) # Line
stampsLabel_knob = nuke.Text_Knob('stamps_label','Stamps:', " ")
stampsLabel_knob.setFlag(nuke.STARTLINE)
# Buttons
buttonSelectStamps = nuke.PyScript_Knob("selectStamps","select","stamps.wiredSelectSimilar(nuke.thisNode().name())")
buttonSelectStamps.setTooltip("Select all of this Anchor's Stamps.")
buttonReconnectStamps = nuke.PyScript_Knob("reconnectStamps","reconnect","stamps.anchorReconnectWired()")
buttonSelectStamps.setTooltip("Reconnect all of this Anchor's Stamps.")
buttonZoomNext = nuke.PyScript_Knob("zoomNext","zoom next","stamps.wiredZoomNext(nuke.thisNode().name())")
buttonZoomNext.setTooltip("Navigate to this Anchor's next Stamp on the Node Graph.")
buttonCreateStamp = nuke.PyScript_Knob("createStamp","new","stamps.stampCreateWired(nuke.thisNode())")
buttonCreateStamp.setTooltip("Create a new Stamp for this Anchor.")
for k in [stampsLabel_knob, buttonCreateStamp, buttonSelectStamps, buttonReconnectStamps, buttonZoomNext]:
n.addKnob(k)
# Version (for future update checks)
n.addKnob(nuke.Text_Knob("line2", "", "")) # Line
buttonHelp = nuke.PyScript_Knob("buttonHelp","Help","stamps.showHelp()")
version_knob = nuke.Text_Knob('version',' ',' Stamps {}'.format(version))
version_knob.setTooltip(VERSION_TOOLTIP)
version_knob.clearFlag(nuke.STARTLINE)
for k in [buttonHelp, version_knob]:
n.addKnob(k)
n["help"].setValue(STAMPS_HELP)
return n
def wired(anchor):
''' Wired Stamp '''
global Stamps_LastCreated
Stamps_LastCreated = anchor.name()
node_type = nodeType(realInput(anchor))
try:
n = nuke.createNode(StampClassesAlt[node_type])
except:
try:
n = nuke.createNode(StampClasses[node_type])
except:
n = nuke.createNode("NoOp")
n["name"].setValue(getAvailableName("Stamp"))
# Set default knob values
for i,j in wired_defaults.items():
try:
n[i].setValue(j)
except:
pass
for k in n.allKnobs():
k.setFlag(0x0000000000000400)
if node_type in WiredClassColors:
n["tile_color"].setValue(WiredClassColors[node_type])
n["onCreate"].setValue(wiredOnCreate_code)
# Inner functionality knobs
wiredTab_knob = nuke.Tab_Knob('wired_tab','Wired Stamp')
identifier_knob = nuke.Text_Knob('identifier','identifier', 'wired')
identifier_knob.setVisible(False)
lock_knob = nuke.Int_Knob('lockCallbacks','',0) #Lock callbacks...
lock_knob.setVisible(False)
toReconnect_knob = nuke.Boolean_Knob("toReconnect")
toReconnect_knob.setVisible(False)
title_knob = nuke.String_Knob('title','Title:', anchor["title"].value())
title_knob.setTooltip("Displayed name on the Node Graph for this Stamp and its Anchor.\nIMPORTANT: This is only for display purposes, and is different from the real/internal name of the Stamps.")
prev_title_knob = nuke.Text_Knob('prev_title','', anchor["title"].value())
prev_title_knob.setVisible(False)
tags_knob = nuke.Text_Knob('tags','Tags:', " ")
tags_knob.setTooltip("Tags of this stamp's Anchor, for information purpose only.\nClick \"show anchor\" to change them.")
backdrops_knob = nuke.Text_Knob('backdrops','Backdrops:', " ")
backdrops_knob.setTooltip("Labels of backdrop nodes which contain this stamp's Anchor.")
anchor_knob = nuke.String_Knob('anchor','Anchor', anchor.name()) # This goes in the advanced part
for k in [wiredTab_knob, identifier_knob, lock_knob, toReconnect_knob, title_knob, prev_title_knob, tags_knob, backdrops_knob]:
n.addKnob(k)
wiredTab_knob.setFlag(0) # Open the tab
n.addKnob(nuke.Text_Knob("line1", "", "")) # Line
### Buttons
# Anchor
anchorLabel_knob = nuke.Text_Knob('anchor_label','Anchor:', " ")
anchorLabel_knob.setFlag(nuke.STARTLINE)
buttonShowAnchor = nuke.PyScript_Knob("show_anchor"," show anchor ","stamps.wiredShowAnchor()")
buttonShowAnchor.setTooltip("Show the properties panel for this Stamp's Anchor.")
buttonShowAnchor.clearFlag(nuke.STARTLINE)
buttonZoomAnchor = nuke.PyScript_Knob("zoom_anchor","zoom anchor","stamps.wiredZoomAnchor()")
buttonZoomAnchor.setTooltip("Navigate to this Stamp's Anchor on the Node Graph.")
for k in [anchorLabel_knob, buttonShowAnchor, buttonZoomAnchor]:
n.addKnob(k)
# Stamps
stampsLabel_knob = nuke.Text_Knob('stamps_label','Stamps:', " ")
stampsLabel_knob.setFlag(nuke.STARTLINE)
buttonZoomNext = nuke.PyScript_Knob("zoomNext"," zoom next ","stamps.wiredZoomNext()")
buttonZoomNext.setTooltip("Navigate to this Stamp's next sibling on the Node Graph.")
buttonZoomNext.clearFlag(nuke.STARTLINE)
buttonSelectSimilar = nuke.PyScript_Knob("selectSimilar"," select similar ","stamps.wiredSelectSimilar()")
buttonSelectSimilar.clearFlag(nuke.STARTLINE)
buttonSelectSimilar.setTooltip("Select all similar Stamps to this one on the Node Graph.")
space_1_knob = nuke.Text_Knob("space_1", "", " ")
space_1_knob.setFlag(nuke.STARTLINE)
for k in [stampsLabel_knob, buttonZoomNext, buttonSelectSimilar, space_1_knob]:
n.addKnob(k)
# Reconnect Simple
reconnectLabel_knob = nuke.Text_Knob('reconnect_label','Reconnect:', " ")
reconnectLabel_knob.setTooltip("Reconnect by the stored Anchor name.")
reconnectLabel_knob.setFlag(nuke.STARTLINE)
buttonReconnectThis = nuke.PyScript_Knob("reconnect_this","this",wiredReconnect_code)
buttonReconnectThis.setTooltip("Reconnect this Stamp to its Anchor, by its stored Anchor name.")
buttonReconnectSimilar = nuke.PyScript_Knob("reconnect_similar","similar","stamps.wiredReconnectSimilar()")
buttonReconnectSimilar.setTooltip("Reconnect this Stamp and similar ones to their Anchor, by their stored anchor name.")
buttonReconnectAll = nuke.PyScript_Knob("reconnect_all","all","stamps.wiredReconnectAll()")
buttonReconnectAll.setTooltip("Reconnect all the Stamps to their Anchors, by their stored anchor names.")
space_2_knob = nuke.Text_Knob("space_2", "", " ")
space_2_knob.setFlag(nuke.STARTLINE)
for k in [reconnectLabel_knob, buttonReconnectThis, buttonReconnectSimilar, buttonReconnectAll, space_2_knob]:
n.addKnob(k)
# Advanced Reconnection
advancedReconnection_knob = nuke.Tab_Knob('advanced_reconnection', 'Advanced Reconnection', nuke.TABBEGINCLOSEDGROUP)
n.addKnob(advancedReconnection_knob)
reconnectByTitleLabel_knob = nuke.Text_Knob('reconnect_by_title_label','By Title:', " ")
reconnectByTitleLabel_knob.setFlag(nuke.STARTLINE)
reconnectByTitleLabel_knob.setTooltip("Reconnect by searching for a matching title.")
buttonReconnectByTitleThis = nuke.PyScript_Knob("reconnect_by_title_this","this","stamps.wiredReconnectByTitle()")
buttonReconnectByTitleThis.setTooltip("Look for an Anchor that shares this Stamp's title, and connect this Stamp to it.\nIMPORTANT: Use this carefully, and only when the normal reconnection doesn't work.")
buttonReconnectByTitleSimilar = nuke.PyScript_Knob("reconnect_by_title_similar","similar","stamps.wiredReconnectByTitleSimilar()")
buttonReconnectByTitleSimilar.setTooltip("Look for an Anchor that shares this Stamp's title, and connect this Stamp and similar ones to it.\nIMPORTANT: Use this carefully, and only when the normal reconnection doesn't work.")
buttonReconnectByTitleSelected = nuke.PyScript_Knob("reconnect_by_title_selected","selected","stamps.wiredReconnectByTitleSelected()")
buttonReconnectByTitleSelected.setTooltip("For each Stamp selected, look for an Anchor that shares its title, and connect to it.\nIMPORTANT: Use this carefully, and only when the normal reconnection doesn't work.")
reconnectBySelectionLabel_knob = nuke.Text_Knob('reconnect_by_selection_label','By Selection:', " ")
reconnectBySelectionLabel_knob.setFlag(nuke.STARTLINE)
reconnectBySelectionLabel_knob.setTooltip("Force reconnect to a selected Anchor.")
buttonReconnectBySelectionThis = nuke.PyScript_Knob("reconnect_by_selection_this","this","stamps.wiredReconnectBySelection()")
buttonReconnectBySelectionThis.setTooltip("Force reconnect this Stamp to a selected Anchor, whatever its name or title.\nIMPORTANT: Use this carefully, and only when the normal reconnection doesn't work.")
buttonReconnectBySelectionSimilar = nuke.PyScript_Knob("reconnect_by_selection_similar","similar","stamps.wiredReconnectBySelectionSimilar()")
buttonReconnectBySelectionSimilar.setTooltip("Force reconnect this Stamp and similar ones to a selected Anchor, whatever its name or title.\nIMPORTANT: Use this carefully, and only when the normal reconnection doesn't work.")
buttonReconnectBySelectionSelected = nuke.PyScript_Knob("reconnect_by_selection_selected","selected","stamps.wiredReconnectBySelectionSelected()")
buttonReconnectBySelectionSelected.setTooltip("Force reconnect all selected Stamps to an also selected Anchor, whatever its name or title.\nIMPORTANT: Use this carefully, and only when the normal reconnection doesn't work.")
checkboxReconnectByTitleOnCreation = nuke.Boolean_Knob("auto_reconnect_by_title"," auto-reconnect by title")
checkboxReconnectByTitleOnCreation.setTooltip("When creating this stamp again (like on copy-paste), auto-reconnect it by title instead of doing it by the saved anchor's name, and auto-turn this off immediately.\nIMPORTANT: Should be off by default. Only use this for setting up templates.")
checkboxReconnectByTitleOnCreation.setFlag(nuke.STARTLINE)
advancedReconnection_knob = nuke.Tab_Knob('advanced_reconnection', 'Advanced Reconnection', -1)
for k in [reconnectByTitleLabel_knob, buttonReconnectByTitleThis, buttonReconnectByTitleSimilar, buttonReconnectByTitleSelected,
reconnectBySelectionLabel_knob, buttonReconnectBySelectionThis, buttonReconnectBySelectionSimilar, buttonReconnectBySelectionSelected,
anchor_knob, checkboxReconnectByTitleOnCreation,advancedReconnection_knob]:
n.addKnob(k)
# Version (for future update checks)
line_knob = nuke.Text_Knob("line2", "", "")
buttonHelp = nuke.PyScript_Knob("buttonHelp","Help","stamps.showHelp()")
version_knob = nuke.Text_Knob('version',' ',' Stamps {}'.format(version))
version_knob.clearFlag(nuke.STARTLINE)
version_knob.setTooltip(VERSION_TOOLTIP)
for k in [line_knob, buttonHelp, version_knob]:
n.addKnob(k)
# Hide input while not messing position
x,y = n.xpos(),n.ypos()
nw = n.screenWidth()
aw = anchor.screenWidth()
n.setInput(0,anchor)
n["hide_input"].setValue(True)
n["xpos"].setValue(x-nw/2+aw/2)
n["ypos"].setValue(y)
n["help"].setValue(STAMPS_HELP)
wiredTagsAndBackdrops(n)
return n
Stamps_LastCreated = anchor.name()
def getAvailableName(name = "Untitled", rand=False):
''' Returns a node name starting with @name followed by a number, that doesn't exist already '''
import random
i = 1
while True:
if not rand:
available_name = name+str(i)
else:
available_name = name+str('_%09x' % random.randrange(9**12))
if not nuke.exists(available_name):
return available_name
i += 1
#################################
### CLASSES
#################################
class AnchorSelector(QtWidgets.QDialog):
'''
Panel to select an anchor, showing the different anchors on dropdowns based on their tags.
'''
# TODO LATER:
# - Have three columns, distinguished, like an asset loader. Maybe even with border color?
# - Button to activate the "Multiple" mode (off by default) which will not close it on clicking "OK"
# - Ability to show and hide backdrops (would turn off their visibility or "bookmark")
def __init__(self):
super(AnchorSelector, self).__init__()
self.setWindowTitle("Stamps: Select an Anchor.")
self.initUI()
#self.setFixedSize(self.sizeHint())
#self.setFixedWidth(self.sizeHint()[0])
self.custom_anchors_lineEdit.setFocus()
def initUI(self):
# Find all anchors and get all tags
self.findAnchorsAndTags() # Generate a dictionary: {"Camera1":["Camera","New","Custom1"],"Read":["2D","New"]}
self.custom_chosen = False # If clicked OK on the custom lineedit
# Header
self.headerTitle = QtWidgets.QLabel("Anchor Stamp Selector")
self.headerTitle.setStyleSheet("font-weight:bold;color:#CCCCCC;font-size:14px;")
self.headerSubtitle = QtWidgets.QLabel("Select an Anchor to make a Stamp for.")
self.headerSubtitle.setStyleSheet("color:#999")
self.headerLine = QtWidgets.QFrame()
self.headerLine.setFrameShape(QtWidgets.QFrame.HLine)
self.headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self.headerLine.setLineWidth(0)
self.headerLine.setMidLineWidth(1)
self.headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
# Layouts
self.master_layout = QtWidgets.QVBoxLayout()
self.master_layout.addWidget(self.headerTitle)
self.master_layout.addWidget(self.headerSubtitle)
#self.master_layout.addWidget(self.headerLine)
# Main Scroll area
self.scroll_content = QtWidgets.QWidget()
self.scroll_layout = QtWidgets.QVBoxLayout()
self.scroll_content.setLayout(self.scroll_layout)
# Scroll Area Properties
self.scroll = QtWidgets.QScrollArea()
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll.setWidgetResizable(True)
self.scroll.setWidget(self.scroll_content)
self.grid = QtWidgets.QGridLayout()
#self.grid.setSpacing(0)
self.lower_grid = QtWidgets.QGridLayout()
self.scroll_layout.addLayout(self.grid)
self.scroll_layout.addStretch()
self.scroll_layout.setContentsMargins(2,2,2,2)
self.grid.setContentsMargins(2,2,2,2)
self.grid.setSpacing(5)
num_tags = len(self._all_tags)
middleLine = QtWidgets.QFrame()
middleLine.setStyleSheet("margin-top:20px")
middleLine.setFrameShape(QtWidgets.QFrame.HLine)
middleLine.setFrameShadow(QtWidgets.QFrame.Sunken)
middleLine.setLineWidth(0)
middleLine.setMidLineWidth(1)
middleLine.setFrameShadow(QtWidgets.QFrame.Sunken)
if len(filter(None,self._all_tags))>0:
tags_label = QtWidgets.QLabel("Tags")
tags_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
tags_label.setStyleSheet("color:#666;margin:0px;padding:0px;padding-left:3px")
#self.grid.addWidget(middleLine,tag_num*10-6,1,2,3)
self.grid.addWidget(tags_label,0,0,1,3)
for tag_num, tag in enumerate(self._all_tags_and_backdrops):
if tag == "":
continue
if tag_num < num_tags:
tag_label = QtWidgets.QLabel("{}:".format(tag))
mode = "tag"
else:
tag_label = QtWidgets.QLabel("{}:".format(tag))
mode = "backdrop"
if tag_num == num_tags:
backdrops_label = QtWidgets.QLabel(" Backdrops")
backdrops_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
backdrops_label.setStyleSheet("color:#666;margin:0px;padding:0px;padding-left:3px")
#self.grid.addWidget(middleLine,tag_num*10-5,0,1,3)
self.grid.addWidget(backdrops_label,tag_num*10-3,0,1,1)
tag_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
anchors_dropdown = QtWidgets.QComboBox()
for i, cur_name in enumerate(self._all_anchors_names):
cur_title = self._all_anchors_titles[i]
title_repeated = self.titleRepeatedForTag(cur_title, tag, mode)
if mode == "tag":
tag_dict = self._anchors_and_tags_tags
elif mode == "backdrop":
tag_dict = self._anchors_and_tags_backdrops
else:
tag_dict = self._anchors_and_tags
if cur_name not in tag_dict.keys():
continue
if tag in tag_dict[cur_name]:
if title_repeated:
anchors_dropdown.addItem("{0} ({1})".format(cur_title, cur_name), cur_name)
else:
anchors_dropdown.addItem(cur_title, cur_name)
ok_btn = QtWidgets.QPushButton("OK")
ok_btn.clicked.connect(partial(self.okPressed,dropdown=anchors_dropdown))
self.grid.addWidget(tag_label,tag_num*10+1,0)
self.grid.addWidget(anchors_dropdown,tag_num*10+1,1)
self.grid.addWidget(ok_btn,tag_num*10+1,2)
# ALL
tag_num = len(self._all_tags_and_backdrops)
all_tag_label = QtWidgets.QLabel("all: ")
all_tag_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.all_anchors_dropdown = QtWidgets.QComboBox()
all_tag_texts = [] # List of all texts of the items (usually equals the "title", or "title (name)")
all_tag_names = [i for i in self._all_anchors_names] # List of all real anchor names of the items.
for i, cur_name in enumerate(self._all_anchors_names):
cur_title = self._all_anchors_titles[i]
title_repeated = self._all_anchors_titles.count(cur_title)
if title_repeated > 1:
all_tag_texts.append("{0} ({1})".format(cur_title, cur_name))
else:
all_tag_texts.append(cur_title)
self.all_tag_sorted = sorted(zip(all_tag_texts,all_tag_names), key=lambda pair: pair[0].lower())
for [text, name] in self.all_tag_sorted:
self.all_anchors_dropdown.addItem(text, name)
all_ok_btn = QtWidgets.QPushButton("OK")
all_ok_btn.clicked.connect(partial(self.okPressed,dropdown=self.all_anchors_dropdown))
self.lower_grid.addWidget(all_tag_label,tag_num,0)
self.lower_grid.addWidget(self.all_anchors_dropdown,tag_num,1)
self.lower_grid.addWidget(all_ok_btn,tag_num,2)
tag_num += 1
# POPULAR
tag_num = len(self._all_tags_and_backdrops)+10
popular_tag_label = QtWidgets.QLabel("popular: ")
popular_tag_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.popular_anchors_dropdown = QtWidgets.QComboBox()
all_tag_texts = [] # List of all texts of the items (usually equals the "title", or "title (name)")
all_tag_names = [i for i in self._all_anchors_names] # List of all real anchor names of the items.
all_tag_count = [stampCount(i) for i in self._all_anchors_names] # Number of stamps for each anchor!
popular_tag_texts = [] # List of popular texts of the items (usually equals the "title (x2)", or "title (name) (x2)")
popular_anchors_names = [x for _,x in sorted(zip(all_tag_count,self._all_anchors_names),reverse=True)]
popular_anchors_titles = [x for _,x in sorted(zip(all_tag_count,self._all_anchors_titles),reverse=True)]
popular_anchors_count = sorted(all_tag_count,reverse=True)
for i, cur_name in enumerate(popular_anchors_names):
cur_title = popular_anchors_titles[i]
title_repeated = popular_anchors_titles.count(cur_title)
if title_repeated > 1:
popular_tag_texts.append("{0} ({1}) (x{2})".format(cur_title, cur_name,str(popular_anchors_count[i])))
else:
popular_tag_texts.append("{0} (x{1})".format(cur_title, str(popular_anchors_count[i])))
for i, text in enumerate(popular_tag_texts):
self.popular_anchors_dropdown.addItem(text, popular_anchors_names[i])
popular_ok_btn = QtWidgets.QPushButton("OK")
popular_ok_btn.clicked.connect(partial(self.okPressed,dropdown=self.popular_anchors_dropdown))
self.lower_grid.addWidget(popular_tag_label,tag_num,0)
self.lower_grid.addWidget(self.popular_anchors_dropdown,tag_num,1)
self.lower_grid.addWidget(popular_ok_btn,tag_num,2)
tag_num += 1
# LineEdit with completer
custom_tag_label = QtWidgets.QLabel("by title: ")
custom_tag_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.custom_anchors_lineEdit = QtWidgets.QLineEdit()
self.custom_anchors_completer = QtWidgets.QCompleter([i for i,_ in self.all_tag_sorted], self)
self.custom_anchors_completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.custom_anchors_completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion)
self.custom_anchors_lineEdit.setCompleter(self.custom_anchors_completer)
global Stamps_LastCreated
if Stamps_LastCreated is not None:
try:
title = nuke.toNode(Stamps_LastCreated)["title"].value()
self.custom_anchors_lineEdit.setPlaceholderText(title)
except:
pass
custom_ok_btn = QtWidgets.QPushButton("OK")
custom_ok_btn.clicked.connect(partial(self.okCustomPressed,dropdown=self.custom_anchors_lineEdit))
self.lower_grid.addWidget(custom_tag_label,tag_num,0)
self.lower_grid.addWidget(self.custom_anchors_lineEdit,tag_num,1)
self.lower_grid.addWidget(custom_ok_btn,tag_num,2)
# Layout shit
self.grid.setColumnStretch(1,1)
if len(filter(None,self._all_tags_and_backdrops)):
self.master_layout.addWidget(self.scroll)
else:
self.master_layout.addWidget(self.headerLine)
self.master_layout.addLayout(self.lower_grid)
self.setLayout(self.master_layout)
self.resize(self.sizeHint().width(),min(self.sizeHint().height()+10,700))
self.setFixedWidth(self.sizeHint().width()+40)
def keyPressEvent(self, e):
selectorType = type(self.focusWidget()).__name__ #QComboBox or QLineEdit
if e.key() == 16777220:
if selectorType == "QLineEdit":
self.okCustomPressed(dropdown=self.focusWidget())
else:
self.okPressed(dropdown=self.focusWidget())
else:
return QtWidgets.QDialog.keyPressEvent(self, e)
def findAnchorsAndTags(self):
# Lets find anchors
self._all_anchors_titles = []
self._all_anchors_names = []
self._all_tags = set()
self._all_backdrops = set()
self._backdrop_item_count = {} # Number of anchors per backdrop
self._all_tags_and_backdrops = set()
self._anchors_and_tags = {} # Name:tags. Not title.
self._anchors_and_tags_tags = {} # Name:tags. Not title.
self._anchors_and_tags_backdrops = {} # Name:tags. Not title.
for ni in allAnchors():
try:
title_value = ni["title"].value()
name_value = ni.name()
tags_value = ni["tags"].value()
tags = re.split(" *, *",tags_value.strip()) # Remove leading/trailing spaces and separate by commas (with or without spaces)
#backdrop_tags = ["$b$"+x for x in backdropTags(ni)]
backdrop_tags = backdropTags(ni)
for t in backdrop_tags:
if t not in self._backdrop_item_count:
self._backdrop_item_count[t] = 0
self._backdrop_item_count[t] += 1
tags_and_backdrops = list(set(tags + backdrop_tags))
self._all_anchors_titles.append(title_value.strip())
self._all_anchors_names.append(name_value)
self._all_tags.update(tags)
self._all_backdrops.update(backdrop_tags)
self._all_tags_and_backdrops.update(tags_and_backdrops)
self._anchors_and_tags[name_value] = set(tags_and_backdrops)
self._anchors_and_tags_tags[name_value] = set(tags)
self._anchors_and_tags_backdrops[name_value] = set(backdrop_tags)
except:
pass
self._all_backdrops = sorted(list(self._all_backdrops), key = lambda x: -self._backdrop_item_count[x])
self._all_tags = sorted(list(self._all_tags),key=str.lower)
self._all_tags_and_backdrops = self._all_tags + self._all_backdrops
#titles_upper = [x.upper() for x in self._all_anchors_titles]
titles_and_names = zip(self._all_anchors_titles, self._all_anchors_names)
titles_and_names.sort(key=lambda tup: tup[0].upper())
self._all_anchors_titles = [x for x, y in titles_and_names]
self._all_anchors_names = [y for x, y in titles_and_names]
return self._anchors_and_tags
def titleRepeatedForTag(self, title, tag, mode=""):
if self._all_anchors_titles.count(title) <= 1:
return False
# Get list of all names that have that tag, and list of related titles
names_with_tag = []
titles_with_tag = []
for i, name in enumerate(self._all_anchors_names):
if mode == "tag":
if tag in self._anchors_and_tags_tags[name]:
names_with_tag.append(name)
titles_with_tag.append(self._all_anchors_titles[i])
elif mode == "backdrop":
if tag in self._anchors_and_tags_backdrops[name]:
names_with_tag.append(name)
titles_with_tag.append(self._all_anchors_titles[i])
else:
if tag in self._anchors_and_tags[name]:
names_with_tag.append(name)
titles_with_tag.append(self._all_anchors_titles[i])
# Count titles repetition
title_repetitions = titles_with_tag.count(title)
return (title_repetitions > 1)
def okPressed(self, dropdown):
''' Runs after an ok button is pressed '''
dropdown_value = dropdown.currentText()
dropdown_index = dropdown.currentIndex()
dropdown_data = dropdown.itemData(dropdown_index)
try:
match_anchor = nuke.toNode(dropdown_data)
except:
pass
self.chosen_value = dropdown_value
self.chosen_anchor_name = dropdown_data
if match_anchor is not None:
self.chosen_anchor = match_anchor
self.accept()
else:
nuke.message("There was a problem selecting a valid anchor.")
def okCustomPressed(self, dropdown):
''' Runs after the custom ok button is pressed '''
global Stamps_LastCreated
written_value = dropdown.text() # This means it's been written down on the lineEdit
written_lower = written_value.lower().strip()
found_data = None
if written_value == "" and globals().has_key('Stamps_LastCreated'):
found_data = Stamps_LastCreated
else:
for [text, name] in reversed(self.all_tag_sorted):
if written_lower == text.lower():
found_data = name
break
elif written_lower in text.lower():
found_data = name
try:
match_anchor = nuke.toNode(found_data)
except:
nuke.message("Please write a valid name.")
return
self.chosen_value = written_value
self.chosen_anchor_name = found_data
if match_anchor is not None:
self.chosen_anchor = match_anchor
self.accept()
else:
nuke.message("There was a problem selecting a valid anchor.")
class AnchorTags_LineEdit(QtWidgets.QLineEdit):
new_text = QtCore.Signal(object, object)
def __init__(self, *args):
QtWidgets.QLineEdit.__init__(self, *args)
self.textChanged.connect(self.text_changed)
self.completer = None
def text_changed(self, text):
all_text = unicode(text)
text = all_text[:self.cursorPosition()]
prefix = text.split(',')[-1].strip()
text_tags = []
for t in all_text.split(','):
t1 = unicode(t).strip()
if t1 != '':
text_tags.append(t.strip())
text_tags = list(set(text_tags))
self.new_text.emit(text_tags, prefix)
def mouseReleaseEvent(self, e):
self.text_changed(self.text())
def complete_text(self, text):
cursor_pos = self.cursorPosition()
before_text = unicode(self.text())[:cursor_pos]
after_text = unicode(self.text())[cursor_pos:]
prefix_len = len(before_text.split(',')[-1].strip())
if after_text.strip() == '':
self.setText('%s%s' % (before_text[:cursor_pos - prefix_len], text))
else:
self.setText('%s%s, %s' % (before_text[:cursor_pos - prefix_len], text, after_text))
self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2)
class TagsCompleter(QtWidgets.QCompleter):
insertText = QtCore.Signal(str)
def __init__(self, all_tags):
QtWidgets.QCompleter.__init__(self, all_tags)
self.all_tags = set(all_tags)
self.activated.connect(self.activated_text)
def update(self, text_tags, completion_prefix):
tags = list(self.all_tags-set(text_tags))
model = QtGui.QStringListModel(tags, self)
self.setModel(model)
self.setCompletionPrefix(completion_prefix)
self.complete()
def activated_text(self,completion):
self.insertText.emit(completion)
class NewAnchorPanel(QtWidgets.QDialog):
'''
Panel to create a new anchor on a selected node, where you can choose the name (autocompleted at first) and tags.
'''
def __init__(self, windowTitle = "New Stamp", default_title="", all_tags = [], default_tags = "", parent = None):
super(NewAnchorPanel, self).__init__(parent)
self.default_title = default_title
self.all_tags = all_tags
self.default_tags = default_tags
self.setWindowTitle(windowTitle)
self.initUI()
self.setFixedSize(self.sizeHint())
def initUI(self):
self.createWidgets()
self.createLayouts()
def createWidgets(self):
"""Create widgets..."""
self.newAnchorTitle = QtWidgets.QLabel("New Anchor Stamp")
self.newAnchorTitle.setStyleSheet("font-weight:bold;color:#CCCCCC;font-size:14px;")
self.newAnchorSubtitle = QtWidgets.QLabel("Set Stamp title and tag/s (comma separated)")
self.newAnchorSubtitle.setStyleSheet("color:#999")
self.newAnchorLine = QtWidgets.QFrame()
self.newAnchorLine.setFrameShape(QtWidgets.QFrame.HLine)
self.newAnchorLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self.newAnchorLine.setLineWidth(0)
self.newAnchorLine.setMidLineWidth(1)
self.newAnchorLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self.anchorTitle_label = QtWidgets.QLabel("Title: ")
self.anchorTitle_edit = QtWidgets.QLineEdit()
self.anchorTitle_edit.setFocus()
self.anchorTitle_edit.setText(self.default_title)
self.anchorTitle_edit.selectAll()
self.anchorTags_label = QtWidgets.QLabel("Tags: ")
self.anchorTags_edit = AnchorTags_LineEdit()
self.anchorTags_edit.setText(self.default_tags)
self.anchorTags_completer = TagsCompleter(self.all_tags)
self.anchorTags_completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.anchorTags_completer.insertText.connect(self.anchorTags_edit.complete_text)
self.anchorTags_edit.new_text.connect(self.anchorTags_completer.update)
self.anchorTags_completer.setWidget(self.anchorTags_edit)
self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self.clickedOk)
self.buttonBox.rejected.connect(self.clickedCancel)
def createLayouts(self):
"""Create layout..."""
self.titleAndTags_layout = QtWidgets.QGridLayout()
self.titleAndTags_layout.addWidget(self.anchorTitle_label,0,0)
self.titleAndTags_layout.addWidget(self.anchorTitle_edit,0,1)
self.titleAndTags_layout.addWidget(self.anchorTags_label,1,0)
self.titleAndTags_layout.addWidget(self.anchorTags_edit,1,1)
self.master_layout = QtWidgets.QVBoxLayout()
self.master_layout.addWidget(self.newAnchorTitle)
self.master_layout.addWidget(self.newAnchorSubtitle)
self.master_layout.addWidget(self.newAnchorLine)
self.master_layout.addLayout(self.titleAndTags_layout)
self.master_layout.addWidget(self.buttonBox)
self.setLayout(self.master_layout)
def clickedOk(self):
self.anchorTitle = self.anchorTitle_edit.text().strip()
if self.anchorTitle == "" and self.anchorTitle_edit.text() != "":
self.anchorTitle = self.anchorTitle_edit.text()
self.anchorTags = self.anchorTags_edit.text().strip()
self.accept()
return True
def clickedCancel(self):
'''Abort mission'''
self.reject()
class AddTagsPanel(QtWidgets.QDialog):
'''
Panel to add tags to the selected Stamps or nodes.
'''
def __init__(self, all_tags = [], default_tags = "", parent = None):
super(AddTagsPanel, self).__init__(parent)
self.all_tags = all_tags
self.allNodes = True
self.default_tags = default_tags
self.setWindowTitle("Stamps: Add Tags")
self.initUI()
self.setFixedSize(self.sizeHint())
def initUI(self):
self.createWidgets()
self.createLayouts()
def createWidgets(self):
self.addTagsTitle = QtWidgets.QLabel("Add tag/s")
self.addTagsTitle.setStyleSheet("font-weight:bold;color:#CCCCCC;font-size:14px;")
self.addTagsSubtitle = QtWidgets.QLabel("Add tag/s to the selected nodes (comma separated).")
self.addTagsSubtitle.setStyleSheet("color:#999")
self.addTagsLine = QtWidgets.QFrame()
self.addTagsLine.setFrameShape(QtWidgets.QFrame.HLine)
self.addTagsLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self.addTagsLine.setLineWidth(0)
self.addTagsLine.setMidLineWidth(1)
self.addTagsLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self.tags_label = QtWidgets.QLabel("Tags: ")
self.tags_edit = AnchorTags_LineEdit()
self.tags_edit.setText(self.default_tags)
self.tags_completer = TagsCompleter(self.all_tags)
self.tags_completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.tags_completer.insertText.connect(self.tags_edit.complete_text)
self.tags_edit.new_text.connect(self.tags_completer.update)
self.tags_completer.setWidget(self.tags_edit)
self.addTo_Label = QtWidgets.QLabel("Add to: ")
self.addTo_Label.setToolTip("Which nodes to add tag/s to.")
self.addTo_btnA = QtWidgets.QRadioButton("All selected nodes")
self.addTo_btnA.setChecked(True)
self.addTo_btnB = QtWidgets.QRadioButton("Selected Stamps")
addTo_ButtonGroup = QtWidgets.QButtonGroup(self)
addTo_ButtonGroup.addButton(self.addTo_btnA)
addTo_ButtonGroup.addButton(self.addTo_btnB)
self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self.clickedOk)
self.buttonBox.rejected.connect(self.clickedCancel)
def createLayouts(self):
self.main_layout = QtWidgets.QGridLayout()
self.main_layout.addWidget(self.tags_label,0,0)
self.main_layout.addWidget(self.tags_edit,0,1)
addTo_Buttons_layout = QtWidgets.QHBoxLayout()
addTo_Buttons_layout.addWidget(self.addTo_btnA)
addTo_Buttons_layout.addWidget(self.addTo_btnB)
self.main_layout.addWidget(self.addTo_Label,1,0)
self.main_layout.addLayout(addTo_Buttons_layout,1,1)
self.master_layout = QtWidgets.QVBoxLayout()
self.master_layout.addWidget(self.addTagsTitle)
self.master_layout.addWidget(self.addTagsSubtitle)
self.master_layout.addWidget(self.addTagsLine)
self.master_layout.addLayout(self.main_layout)
self.master_layout.addWidget(self.buttonBox)
self.setLayout(self.master_layout)
def clickedOk(self):
'''Check if all is correct and submit'''
self.tags = self.tags_edit.text().strip()
self.allNodes = self.addTo_btnA.isChecked
self.accept()
return True
def clickedCancel(self):
'''Abort mission'''
self.reject()
class RenameTagPanel(QtWidgets.QDialog):
'''
Panel to rename a tag on the selected (or all) nodes.
'''
def __init__(self, all_tags = [], default_tags = "", parent = None):
super(RenameTagPanel, self).__init__(parent)
self.all_tags = all_tags
self.allNodes = True
self.default_tags = default_tags
self.setWindowTitle("Stamps: Rename tag")
self.initUI()
self.setFixedSize(self.sizeHint())
def initUI(self):
self.createWidgets()
self.createLayouts()
def createWidgets(self):
self.headerTitle = QtWidgets.QLabel("Rename tag")
self.headerTitle.setStyleSheet("font-weight:bold;color:#CCCCCC;font-size:14px;")
self.headerSubtitle = QtWidgets.QLabel("Rename a tag on the selected (or all) nodes.")
self.headerSubtitle.setStyleSheet("color:#999")
self.headerLine = QtWidgets.QFrame()
self.headerLine.setFrameShape(QtWidgets.QFrame.HLine)
self.headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self.headerLine.setLineWidth(0)
self.headerLine.setMidLineWidth(1)
self.headerLine.setFrameShadow(QtWidgets.QFrame.Sunken)
self.tag_label = QtWidgets.QLabel("Rename: ")
self.tag_edit = QtWidgets.QLineEdit()
all_tags = allTags()
self.tag_completer = QtWidgets.QCompleter(all_tags, self)
self.tag_completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.tag_edit.setCompleter(self.tag_completer)
self.tagReplace_label = QtWidgets.QLabel("To: ")
self.tagReplace_edit = QtWidgets.QLineEdit()
self.tagReplace_completer = QtWidgets.QCompleter(all_tags, self)
self.tagReplace_completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.tagReplace_edit.setCompleter(self.tagReplace_completer)
self.addTo_Label = QtWidgets.QLabel("Apply on: ")
self.addTo_Label.setToolTip("Which nodes to rename the tag on.")
self.addTo_btnA = QtWidgets.QRadioButton("Selected nodes")
self.addTo_btnA.setChecked(True)
self.addTo_btnB = QtWidgets.QRadioButton("All nodes")
addTo_ButtonGroup = QtWidgets.QButtonGroup(self)
addTo_ButtonGroup.addButton(self.addTo_btnA)
addTo_ButtonGroup.addButton(self.addTo_btnB)
self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self.clickedOk)
self.buttonBox.rejected.connect(self.clickedCancel)
def createLayouts(self):
self.main_layout = QtWidgets.QGridLayout()
self.main_layout.addWidget(self.tag_label,0,0)
self.main_layout.addWidget(self.tag_edit,0,1)
self.main_layout.addWidget(self.tagReplace_label,1,0)
self.main_layout.addWidget(self.tagReplace_edit,1,1)
addTo_Buttons_layout = QtWidgets.QHBoxLayout()
addTo_Buttons_layout.addWidget(self.addTo_btnA)
addTo_Buttons_layout.addWidget(self.addTo_btnB)
self.main_layout.addWidget(self.addTo_Label,2,0)
self.main_layout.addLayout(addTo_Buttons_layout,2,1)
self.master_layout = QtWidgets.QVBoxLayout()
self.master_layout.addWidget(self.headerTitle)
self.master_layout.addWidget(self.headerSubtitle)
self.master_layout.addWidget(self.headerLine)
self.master_layout.addLayout(self.main_layout)
self.master_layout.addWidget(self.buttonBox)
self.setLayout(self.master_layout)
def clickedOk(self):
'''Check if all is correct and submit'''
self.tag = self.tag_edit.text().strip()
self.tagReplace = self.tagReplace_edit.text().strip()
self.allNodes = self.addTo_btnB.isChecked
self.accept()
return True
def clickedCancel(self):
'''Abort mission'''
self.reject()
#################################
### FUNCTIONS
#################################
def getDefaultTitle(node = None):
if node == None:
return False
title = str(node.name())
# Default defaults here
# cam
if "Camera" in node.Class() and not any([(i.knob("title") and i["title"].value() == "cam") for i in nuke.allNodes("NoOp")]):
title = "cam"
return title
if node.Class() in ["Dot","NoOp"] and node["label"].value().strip() != "":
return node["label"].value().strip()
# Filename
try:
file = node['file'].value()
if not (node.knob("read_from_file") and not node["read_from_file"].value()):
if file != "":
rawname = file.rpartition('/')[2].rpartition('.')[0]
if '.' in rawname:
rawname = rawname.rpartition('.')[0]
# 1: beauty?
m = re.match(r"([\w]+)_v[\d]+_beauty", rawname)
if m:
pre_version = m.groups()[0]
title = "_".join(pre_version.split("_")[3:])
return title
# 2: Other
rawname = str(re.split("_v[0-9]*_",rawname)[-1]).replace("_render","")
title = rawname
except:
pass
return title
def backdropTags(node = None):
''' Returns the list of words belonging to the backdrop/s label/s'''
backdrops = findBackdrops(node)
tags = []
for b in backdrops:
if b.knob("visible_for_stamps"):
if not b["visible_for_stamps"].value():
continue
elif not b["bookmark"].value():
continue
label = b["label"].value()
if len(label) and len(label) < 50 and not label.startswith("\\"):
label = label.split("\n")[0].strip()
label = re.sub("<[^<>]>","",label)
label = re.sub("[\s]+"," ",label)
label = re.sub("\.$","",label)
label = label.strip()
tags.append(label)
return tags
def stampCreateAnchor(node = None, extra_tags = [], no_default_tag = False):
'''
Create a stamp from any nuke node.
Returns: extra_tags list is success, None if cancelled
'''
ns = nuke.selectedNodes()
for n in ns:
n.setSelected(False)
if node is not None:
node.setSelected(True)
default_title = getDefaultTitle(realInput(node,stopOnLabel=True,mode="title"))
default_tags = list(set([nodeType(realInput(node,mode="tags"))]))
node_type = nodeType(realInput(node))
window_title = "New Stamp: "+str(node.name())
else:
default_title = "Stamp"
default_tags = ""
node_type = ""
window_title = "New Stamp"
try:
custom_default_title = defaultTitle(node)
if custom_default_title:
default_title = str(custom_default_title)
except:
pass
try:
custom_default_tags = defaultTags(node)
if custom_default_tags:
if KEEP_ORIGINAL_TAGS:
default_tags += custom_default_tags
else:
default_tags = custom_default_tags
except:
pass
default_default_tags = default_tags
if no_default_tag:
default_tags = ", ".join(extra_tags + [""])
else:
default_tags = filter(None,list(dict.fromkeys(default_tags + extra_tags)))
default_tags = ", ".join(default_tags + [""])
global new_anchor_panel
new_anchor_panel = NewAnchorPanel(window_title, default_title, allTags(), default_tags)
while True:
if new_anchor_panel.exec_():
anchor_title = new_anchor_panel.anchorTitle
anchor_tags = new_anchor_panel.anchorTags
if not titleIsLegal(anchor_title):
nuke.message("Please set a valid title.")
continue
elif len(findAnchorsByTitle(anchor_title)):
if not nuke.ask("There is already a Stamp titled "+anchor_title+".\nDo you still want to use this title?"):
continue
na = anchor(title = anchor_title, tags = anchor_tags, input_node = node, node_type = node_type)
na.setYpos(na.ypos()+20)
stampCreateWired(na)
for n in ns:
n.setSelected(True)
node.setSelected(False)
extra_tags = anchor_tags.split(",")
extra_tags = [t.strip() for t in extra_tags if t.strip() not in default_default_tags]
break
else:
break
return extra_tags
def stampSelectAnchor():
'''
Panel to select a stamp anchor (if there are any)
Returns: selected anchor node, or None.
'''
# 1.Get position where to make the child...
nodeForPos = nuke.createNode("NoOp")
childNodePos = [nodeForPos.xpos(),nodeForPos.ypos()]
nuke.delete(nodeForPos)
# 2.Choose the anchor...
anchorList = [n.name() for n in allAnchors()]
if not len(anchorList):
nuke.message("Please create some stamps first...")
return None
else:
global select_anchor_panel
select_anchor_panel = AnchorSelector()
if select_anchor_panel.exec_(): # If user clicks OK
chosen_anchor = select_anchor_panel.chosen_anchor
if chosen_anchor:
return chosen_anchor
return None
def stampCreateWired(anchor = ""):
''' Create a wired stamp from an anchor node. '''
global Stamps_LastCreated
if anchor == "":
anchor = stampSelectAnchor()
if anchor == None:
return
nw = wired(anchor = anchor)
nw.setInput(0,anchor)
else:
ns = nuke.selectedNodes()
for n in ns:
n.setSelected(False)
dot = nuke.nodes.Dot()
dot.setXYpos(anchor.xpos(),anchor.ypos())
dot.setInput(0,anchor)
nw = wired(anchor = anchor)
code = "dummy = nuke.nodes.{}()".format(nw.Class())
exec(code)
nww = dummy.screenWidth()
nuke.delete(dummy)
nuke.delete(dot)
for n in ns:
n.setSelected(True)
nw.setXYpos(anchor.xpos()+anchor.screenWidth()/2-nww/2 ,anchor.ypos()+56)
anchor.setSelected(False)
return nw
def stampCreateByTitle(title = ""):
''' Create a wired stamp given a title. '''
global Stamps_LastCreated
anchor = None
for a in allAnchors():
if a.knob("title") and a["title"].value() == title:
anchor = a
break
if anchor == None:
return
nw = wired(anchor = anchor)
nw.setInput(0,anchor)
return nw
def stampDuplicateWired(wired = ""):
''' Create a duplicate of a wired stamp '''
ns = nuke.selectedNodes()
for n in ns:
n.setSelected(False)
wired.setSelected(True)
clipboard = QtWidgets.QApplication.clipboard()
ctext = clipboard.text()
nuke.nodeCopy("%clipboard%")
wired.setSelected(False)
new_wired = nuke.nodePaste("%clipboard%")
clipboard.setText(ctext)
new_wired.setXYpos(wired.xpos()-110,wired.ypos()+55)
try:
new_wired.setInput(0,wired.input(0))
except:
pass
for n in ns:
n.setSelected(True)
wired.setSelected(False)
def stampType(n = ""):
''' Returns the identifier value if it exists, otherwise False. '''
if isAnchor(n):
return "anchor"
elif isWired(n):
return "wired"
else:
return False
def nodeType(n=""):
'''Returns the node type: Camera, Deep, 3D, Particles, Image or False'''
try:
nodeClass = n.Class()
except:
return False
if nodeClass.startswith("Deep") and nodeClass not in DeepExceptionClasses:
return "Deep"
elif nodeClass.startswith("Particle") and nodeClass not in ParticleExceptionClasses:
return "Particle"
elif nodeClass in ["Camera", "Camera2"]:
return "Camera"
elif nodeClass in ["Axis", "Axis2"]:
return "Axis"
elif (n.knob("render_mode") and n.knob("display")) or nodeClass in ["Axis2","GeoNoOp","EditGeo"]:
return "3D"
else:
return "2D"
def allAnchors(selection=""):
nodes = nuke.allNodes()
if selection == "":
anchors = [a for a in nodes if isAnchor(a)]
else:
anchors = [a for a in nodes if a in selection and isAnchor(a)]
return anchors
def allWireds(selection=""):
nodes = nuke.allNodes()
if selection == "":
wireds = [a for a in nodes if isWired(a)]
else:
wireds = [a for a in nodes if a in selection and isWired(a)]
return wireds
def totalAnchors(selection=""):
num_anchors = len(allAnchors(selection))
return num_anchors
def allTags(selection=""):
all_tags = set()
for ni in allAnchors():
try:
tags_value = ni["tags"].value()
tags = re.split(" *, *",tags_value.strip()) # Remove leading/trailing spaces and separate by commas (with or without spaces)
all_tags.update(tags)
except:
pass
all_tags = filter(None,list(all_tags))
all_tags.sort(key=str.lower)
return all_tags
def findAnchorsByTitle(title = "", selection=""):
''' Returns list of nodes '''
if title == "":
return None
if selection == "":
found_anchors = [a for a in allAnchors() if a.knob("title") and a.knob("title").value() == title]
else:
found_anchors = [a for a in selection if a in allAnchors() and a.knob("title") and a.knob("title").value() == title]
return found_anchors
def titleIsLegal(title=""):
'''
Convenience function to determine which stamp titles are legal.
titleIsLegal(title) -> True or False
'''
if not title or title == "":
return False
return True
def isAnchor(node=""):
''' True or False '''
return True if all(node.knob(i) for i in ["identifier","title"]) and node["identifier"].value() == "anchor" else False
def isWired(node=""):
''' True or False '''
return True if all(node.knob(i) for i in ["identifier","title"]) and node["identifier"].value() == "wired" else False
def findBackdrops(node = ""):
''' Returns a list containing the backdrops that contain the given node.'''
if node =="":
return []
x = node.xpos()
y = node.ypos()
w = node.screenWidth()
h = node.screenHeight()
backdrops = []
for b in nuke.allNodes("BackdropNode"):
bx = int(b['xpos'].value())
by = int(b['ypos'].value())
br = int(b['bdwidth'].value())+bx
bt =int(b['bdheight'].value())+by
if x >= bx and (x+w) <= br and y > by and (y+h) <= bt:
backdrops.append(b)
return backdrops
def realInput(node, stopOnLabel=False, mode=""):
'''
Returns the first input node that is not a Dot or a Stamp
stopOnLabel=False. True: Stop when it's a dot or NoOp but it has a label.
mode="title": ignores TitleIgnoreClasses
mode="tags": ignores TagsIgnoreClasses
'''
try:
n = node
if stampType(n) or n.Class() in InputIgnoreClasses or (mode=="title" and n.Class() in TitleIgnoreClasses) or (mode=="tags" and n.Class() in TagsIgnoreClasses):
if stopOnLabel and n.knob("label") and n["label"].value().strip()!="":
return n
if n.input(0):
n = n.input(0)
return realInput(n,stopOnLabel,mode)
else:
return n
else:
return n
except:
return node
def nodeToScript(node = ""):
''' Returns a node as a tcl string, similar as nodecopy without messing with the clipboard '''
orig_sel_nodes = nuke.selectedNodes()
if node == "":
node = nuke.selectedNode()
if not node:
return ""
for i in orig_sel_nodes:
i.setSelected(False)
node.setSelected(True)
clipboard = QtWidgets.QApplication.clipboard()
ctext = clipboard.text()
nuke.nodeCopy("%clipboard%")
node_as_script = clipboard.text()
clipboard.setText(ctext)
node.setSelected(False)
for i in orig_sel_nodes:
i.setSelected(True)
return node_as_script
def nodesFromScript(script = ""):
''' Returns string as a node, similar as nodepaste without messing with the clipboard '''
if script == "":
return
clipboard = QtWidgets.QApplication.clipboard()
ctext = clipboard.text()
clipboard.setText(script)
nuke.nodePaste("%clipboard%")
clipboard.setText(ctext)
return True
def stampCount(anchor_name=""):
if anchor_name=="":
return len(allWireds())
stamps = [s for s in allWireds() if s["anchor"].value() == anchor_name]
return len(stamps)
def toNoOp(node=""):
'''Turns a given node into a NoOp that shares everything else'''
global Stamps_LockCallbacks
Stamps_LockCallbacks = True
if node == "":
return
if node.Class() == "NoOp":
return
nsn = nuke.selectedNodes()
[i.setSelected(False) for i in nsn]
scr = nodeToScript(node)
scr = re.sub(r"\n[\s]*[\w]+[\s]*{\n","\nNoOp {\n",scr)
scr = re.sub(r"^[\s]*[\w]+[\s]*{\n","NoOp {\n",scr)
legal_starts = ["set","version","push","NoOp","help","onCreate","name","knobChanged","autolabel","tile_color","gl_color","note_font","selected","hide_input"]
scr_split = scr.split("addUserKnob",1)
scr_first = scr_split[0].split("\n")
for i, line in enumerate(scr_first):
if not any([line.startswith(x) or line.startswith(" "+x) for x in legal_starts]):
scr_first[i] = ""
scr_first = "\n".join(filter(None,scr_first)+[""])
scr_split[0] = scr_first
scr = "addUserKnob".join(scr_split)
node.setSelected(True)
xp = node.xpos()
yp = node.ypos()
xw = node.screenWidth()/2
d = nuke.createNode("Dot")
d.setInput(0,node)
inp = None
[i.setSelected(True) for i in nsn]
nuke.delete(node)
d.setSelected(False)
d.setSelected(True)
d.setXYpos(xp+xw-d.screenWidth()/2,yp-18)
nodesFromScript(scr)
n = nuke.selectedNode()
n.setXYpos(xp,yp)
nuke.delete(d)
for i in nsn:
try:
i.setSelected(True)
except:
pass
Stamps_LockCallbacks = False
def allToNoOp():
''' Turns all the stamps into NoOps '''
for n in nuke.allNodes():
if stampType(n) and n.Class() != "NoOp":
toNoOp(n)
#################################
### Menu functions
#################################
def refreshStamps(ns=""):
'''Refresh all the wired Stamps in the script, to spot any wrong styles or connections.'''
stamps = allWireds(ns)
x = 0
failed = []
failed_names = []
for s in stamps:
if not wiredReconnect(s):
x += 1 # Errors
failed.append(s)
else:
try:
s["reconnect_this"].execute()
except:
pass
failed_names = ", ".join([i.name() for i in failed])
if x==0:
if ns == "":
nuke.message("All Stamps refreshed! No errors detected.")
else:
nuke.message("Selected Stamps refreshed! No errors detected.")
else:
[i.setSelected(False) for i in nuke.selectedNodes()]
[i.setSelected(True) for i in failed]
if ns == "":
nuke.message("All Stamps refreshed. Found {0} connection error/s:\n\n{1}".format(str(x), failed_names))
else:
nuke.message("Selected Stamps refreshed. Found {0} connection error/s:\n\n{1}".format(str(x), failed_names))
def addTags(ns=""):
if ns=="":
ns = nuke.selectedNodes()
if not len(ns):
if not nuke.ask("Nothing is selected. Do you wish to add tags to ALL nodes in the script?"):
return
ns = nuke.allNodes()
global stamps_addTags_panel
stamps_addTags_panel = AddTagsPanel(all_tags = allTags(), default_tags = "")
if stamps_addTags_panel.exec_():
all_nodes = stamps_addTags_panel.allNodes
added_tags = stamps_addTags_panel.tags.strip()
added_tags = re.split(r"[\s]*,[\s]*", added_tags)
i = 0
for n in ns:
if isAnchor(n):
tags_knob = n.knob("tags")
elif isWired(n):
a_name = n.knob("anchor").value()
if nuke.exists(a_name):
a = nuke.toNode(a_name)
if a in ns or not isAnchor(a):
continue
tags_knob = a.knob("tags")
elif all_nodes and n.Class() not in NodeExceptionClasses:
tags_knob = n.knob("stamp_tags")
if not tags_knob:
tags_knob = nuke.String_Knob('stamp_tags','Stamp Tags', "")
tags_knob.setTooltip("Stamps: Comma-separated tags you can define for each Anchor, that will help you find it when invoking the Stamp Selector by pressing the Stamps shortkey with nothing selected.")
n.addKnob(tags_knob)
else:
continue
existing_tags = re.split(r"[\s]*,[\s]*", tags_knob.value().strip())
merged_tags = filter(None,list(set(existing_tags + added_tags)))
tags_knob.setValue(", ".join(merged_tags))
i += 1
continue
if i>0:
if all_nodes:
nuke.message("Added the specified tag/s to {} nodes.".format(str(i)))
else:
nuke.message("Added the specified tag/s to {} Anchor Stamps.".format(str(i)))
return
def renameTag(ns=""):
if ns=="":
ns = nuke.selectedNodes()
if not len(ns):
ns = nuke.allNodes()
global stamps_renameTag_panel
stamps_renameTag_panel = RenameTagPanel(all_tags = allTags())
if stamps_renameTag_panel.exec_():
all_nodes = stamps_renameTag_panel.allNodes
if all_nodes:
ns = nuke.allNodes()
added_tag = str(stamps_renameTag_panel.tag.strip())
added_tagReplace = str(stamps_renameTag_panel.tagReplace.strip())
i = 0
for n in ns:
if isAnchor(n):
tags_knob = n.knob("tags")
elif isWired(n):
a_name = n.knob("anchor").value()
if nuke.exists(a_name):
a = nuke.toNode(a_name)
if a in ns or not isAnchor(a):
continue
tags_knob = a.knob("tags")
elif n.Class() not in NodeExceptionClasses:
tags_knob = n.knob("stamp_tags")
if not tags_knob:
continue
else:
continue
existing_tags = filter(None,re.split(r"[\s]*,[\s]*", tags_knob.value()))
added_tag_list = re.split(r"[\s]*,[\s]*", added_tag.strip())
added_tagReplace_list = re.split(r"[\s]*,[\s]*", added_tagReplace)
merged_tags = existing_tags
for atag in added_tag_list:
for rtag in added_tagReplace_list:
merged_tags = [rtag if x == atag else x for x in merged_tags]
merged_tags = filter(None,merged_tags)
if merged_tags != existing_tags:
tags_knob.setValue(", ".join(merged_tags))
i += 1
continue
if i>0:
nuke.message("Renamed the specified tag on {} nodes.".format(str(i)))
return
def selectedReconnectByName():
ns = [n for n in nuke.selectedNodes() if isWired(n)]
for n in ns:
try:
n["reconnect_this"].execute()
except:
pass
def selectedReconnectByTitle():
ns = [n for n in nuke.selectedNodes() if isWired(n)]
for n in ns:
try:
n["reconnect_by_title_this"].execute()
except:
pass
def selectedReconnectBySelection():
ns = [n for n in nuke.selectedNodes() if isWired(n)]
for n in ns:
try:
n["reconnect_by_selection_this"].execute()
except:
pass
def selectedToggleAutorec():
ns = [n for n in nuke.selectedNodes() if n.knob("auto_reconnect_by_title")]
if any([not n.knob("auto_reconnect_by_title").value() for n in ns]):
if nuke.ask("Are you sure you want to set auto-reconnect by title True on all the selected stamps?"):
count = 0
for n in ns:
n.knob("auto_reconnect_by_title").setValue(1)
count += 1
nuke.message("auto-reconnect by title is now True on {} selected stamps.".format(str(count)))
else:
count = 0
for n in ns:
n.knob("auto_reconnect_by_title").setValue(0)
count += 1
nuke.message("auto-reconnect by title is now False on {} selected stamps.".format(str(count)))
def selectedSelectSimilar():
ns = [n for n in nuke.selectedNodes() if isWired(n) or isAnchor(n)]
for n in ns:
try:
if n.knob("selectSimilar"):
n["selectSimilar"].execute()
if n.knob("selectStamps"):
n["selectStamps"].execute()
except:
pass
def showInNukepedia():
from webbrowser import open as openUrl
openUrl("http://www.nukepedia.com/gizmos/other/stamps")
def showInGithub():
from webbrowser import open as openUrl
openUrl("https://github.com/adrianpueyo/stamps")
def showHelp():
from webbrowser import open as openUrl
openUrl("http://www.adrianpueyo.com/Stamps_v1.0.pdf")
def showVideo():
from webbrowser import open as openUrl
openUrl("https://vimeo.com/adrianpueyo/knobscripter2")
def stampBuildMenus():
global Stamps_MenusLoaded
if not Stamps_MenusLoaded:
Stamps_MenusLoaded = True
m = nuke.menu('Nuke')
m.addCommand('Edit/Stamps/Make Stamp', 'stamps.goStamp()', STAMPS_SHORTCUT, icon="stamps.png")
m.addCommand('Edit/Stamps/Add tag\/s to selected nodes', 'stamps.addTags()')
m.addCommand('Edit/Stamps/Rename Stamp tag', 'stamps.renameTag()')
m.addCommand('Edit/Stamps/Refresh all Stamps', 'stamps.refreshStamps()')
m.addCommand('Edit/Stamps/Selected/Reconnect by Name', 'stamps.selectedReconnectByName()')
m.addCommand('Edit/Stamps/Selected/Reconnect by Title', 'stamps.selectedReconnectByTitle()')
m.addCommand('Edit/Stamps/Selected/Reconnect by Selection', 'stamps.selectedReconnectBySelection()')
m.menu('Edit').menu('Stamps').menu('Selected').addSeparator()
m.addCommand('Edit/Stamps/Selected/Refresh', 'stamps.refreshStamps(nuke.selectedNodes())')
m.addCommand('Edit/Stamps/Selected/Select Similar', 'stamps.selectedSelectSimilar()')
m.addCommand('Edit/Stamps/Selected/Toggle auto-rec... by title ', 'stamps.selectedToggleAutorec()')
m.addCommand('Edit/Stamps/Advanced/Convert all Stamps to NoOp', 'stamps.allToNoOp()')
m.menu('Edit').menu('Stamps').addSeparator()
m.addCommand('Edit/Stamps/GitHub', 'stamps.showInGithub()')
m.addCommand('Edit/Stamps/Nukepedia', 'stamps.showInNukepedia()')
m.menu('Edit').menu('Stamps').addSeparator()
m.addCommand('Edit/Stamps/Video tutorials', 'stamps.showVideo()')
m.addCommand('Edit/Stamps/Documentation (.pdf)', 'stamps.showHelp()')
nuke.menu('Nodes').addCommand('Other/Stamps', 'stamps.goStamp()', STAMPS_SHORTCUT, icon="stamps.png")
#################################
### MAIN IMPLEMENTATION
#################################
def goStamp(ns=""):
''' Main stamp function, the one that is called when pressing the main shortcut. '''
if ns=="":
ns = nuke.selectedNodes()
if not len(ns):
if not totalAnchors(): # If no anchors on the script, create an anchor with no input
stampCreateAnchor(no_default_tag = True)
else:
stampCreateWired() # Selection panel in order to create a stamp
return
elif len(ns) == 1 and ns[0].Class() in NodeExceptionClasses:
if not totalAnchors(): # If no anchors on the script, return
return
else:
stampCreateWired() # Selection panel in order to create a stamp
return
else:
# Warn if the selection is too big
if len(ns) > 10 and not nuke.ask("You have "+str(len(ns))+" nodes selected.\nDo you want make stamps for all of them?"):
return
# Main loop
extra_tags = []
for n in ns:
if n in NodeExceptionClasses:
continue
elif isAnchor(n):
stampCreateWired(n) # Make a child to n
elif isWired(n):
stampDuplicateWired(n) # Make a copy of n next to it
else:
if n.knob("stamp_tags"):
stampCreateAnchor(n, extra_tags = n.knob("stamp_tags").value().split(","), no_default_tag = True)
else:
extra_tags = stampCreateAnchor(n, extra_tags = extra_tags) # Create anchor via anchor creation panel
if "Cryptomatte" in n.Class() and n.knob("matteOnly"):
n['matteOnly'].setValue(1)
stampBuildMenus()