#------------------------------------------------------
# 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("<i>{}</i>".format(a_tags))
                    tags_knob.setVisible(True)

                if len(a_bd) and a_bd != []:
                    bd_knob.setValue("<i>{}</i>".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',' ','<a href="http://www.nukepedia.com/gizmos/other/stamps" style="color:#666;text-decoration: none;"><span style="color:#666"> <big>Stamps {}</big></b></a>'.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','<font color=gold>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','<font color=orangered>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","<font color=#ED9977>&nbsp; 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',' ','<a href="http://www.nukepedia.com/gizmos/other/stamps" style="color:#666;text-decoration: none;"><span style="color:#666"> <big>Stamps {}</big></b></a>'.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("<i>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("<b>{}</b>:".format(tag))
                mode = "tag"
            else:
                tag_label = QtWidgets.QLabel("{}:".format(tag))
                mode = "backdrop"
            if tag_num == num_tags:
                backdrops_label = QtWidgets.QLabel(" <i> 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("<b>all</b>: ")
        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("<b>popular</b>: ")
        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("<b>by title</b>: ")
        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 <b>auto-reconnect by title</b> True on all the selected stamps?"):
            count = 0
            for n in ns:
                n.knob("auto_reconnect_by_title").setValue(1)
                count += 1
            nuke.message("<b>auto-reconnect by title</b> 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("<b>auto-reconnect by title</b> is now <b>False</b> 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()
