from xml.etree import ElementTree import nuke import nukescripts import os import re class SearchReplacePanel( nukescripts.PythonPanel ): def __init__( self, historyFile='~/.nuke/srhistory.xml', maxSteps=10 ): ''' Search and Replace panel args: historyFile - file to manage recent search&replace actions maxSteps - amount of steps to keep in history file ''' nukescripts.PythonPanel.__init__( self, 'Search and Replace', 'com.ohufx.SearchReplace') # VARS self.historyFile = os.path.expandvars( os.path.expanduser( historyFile ) ) self.maxSteps = maxSteps self.delimiter = ' |>>| ' # CREATE KNOBS self.nodesChoice = nuke.Enumeration_Knob( 'nodes', 'Source Nodes', ['all', 'selected']) self.nodesChoice.setTooltip( 'Chose to perform action on all nodes with file knobs or only selected ones' ) self.history = nuke.Enumeration_Knob( 'history', 'Recent Searches', self.loadHistory() ) self.history.setTooltip( 'Use the history to quicky access previous search&replace actions.\n By default the history file is stored as "~/.nuke/srhistory.xml" but this can be changed via the "historyFile" argument when creating the panel object. It is also possible to change the size of the history via the "maxSteps" argument to the panel object. Default is 10' ) self.case = nuke.Boolean_Knob( 'case', 'case sensitive' ) self.case.setFlag( nuke.STARTLINE ) self.case.setValue( True ) self.case.setTooltip( 'Set whether or not the search should be case sensitive' ) self.searchStr = nuke.String_Knob('searchStr', 'Search for:') self.searchStr.setTooltip( 'The text to search for' ) self.update = nuke.PyScript_Knob('update', 'Update') self.update.setTooltip( 'update the search result and preview. This is automaticaly performed and usually should only be required when the node selection has canged' ) self.replaceStr = nuke.String_Knob('replaceStr', 'Replace with:') self.replaceStr.setTooltip( 'Text to replace the found text with' ) self.replace = nuke.PyScript_Knob('replace', 'Replace') self.replace.setTooltip( 'Perform replace action. The preview will update afterwards and the action is added to the history' ) self.info = nuke.Multiline_Eval_String_Knob('info', 'Found') self.info.setEnabled( False ) self.info.setTooltip( 'See the search results and a preview of the replace action before it is performed' ) # ADD KNOBS for k in ( self.nodesChoice, self.history, self.case, self.searchStr, self.update, self.replaceStr, self.replace, self.info): self.addKnob( k ) self.matches = None def loadHistory( self ): '''load history file to update knob''' print("loading search&replace history") # GET EXISTING HISTORY if not os.path.isfile( self.historyFile ): return [] # READ EXISTING FILE xmlTree = ElementTree.parse( self.historyFile ) itemList = ['%s%s%s' % ( n.attrib['search'], self.delimiter, n.attrib['replace'] ) for n in xmlTree.findall( 'ITEM' )][-self.maxSteps:] itemList.reverse() itemList.insert( 0, '-- select --' ) return itemList def updateHistory( self, sString, rString ): ''' updates history file args: sString - search string to add to history rString - replace string to add to history TODO - IMPLEMENT MAX VALUE ''' itemList = [] # READ EXISTING FILE if os.path.isfile( self.historyFile ): xmlTree = ElementTree.parse( self.historyFile ) for n in xmlTree.findall( 'ITEM' ): attr = n.attrib itemList.append( attr ) # IGNORE ATTRIBUTES THAT ARE ALREADY IN HISTORY entryExists = False for i in itemList: if i['search'] == sString and i['replace']==rString: entryExists = True break # IF ATTRIBUTES DONT EXIST IN HISTORY, ADD THEM if not entryExists: # PREP DICTIONARY FOR XML DUMP srItem = dict(search=sString, replace=rString ) itemList.append( srItem ) # BUILD XML TREE root = ElementTree.Element( 'SearchReplacePanel') for i in itemList[-self.maxSteps:]: ElementTree.SubElement( root, 'ITEM', attrib=i ) tree = ElementTree.ElementTree( root ) # DUMP XML TREE print("WRITING TO:", self.historyFile) tree.write( self.historyFile ) def search( self, searchstr, nodes ): """ Search in nodes with file knobs. """ fileKnobNodes = [i for i in nodes if self.__NodeHasKnobWithName(i, 'file')] proxyKnobNodes = [i for i in nodes if self.__NodeHasKnobWithName(i, 'proxy')] if not fileKnobNodes and not proxyKnobNodes: raise ValueError("No file nodes selected") nodeMatches = [] knobMatches = [] for i in fileKnobNodes: if self.__findNode(searchstr, i['file'] ): nodeMatches.append( i ) knobMatches.append( i['file'] ) for i in proxyKnobNodes: if self.__findNode(searchstr, i['proxy'] ): nodeMatches.append( i ) knobMatches.append( i['proxy'] ) return nodeMatches, knobMatches def getSearchResults( self, nodes ): # PERFORM SEARCH AND UPDATE INFO KNOB nodeMatches, knobMatches = self.search( self.searchStr.value(), nodes ) nodes = [n.name() for n in nodeMatches] infoStr1 = '%s node(s) found:\n\t%s' % ( len(nodes), ', '.join( nodes ) ) infoStr2 = '' for k in knobMatches: newStr = nukescripts.replaceHashes( self.__doReplace( k ) ) # CHECK IF PATH IS VALID FOR CURRENT FRAME curFrame = int( nuke.Root()['frame'].value() ) # there is a bug which prevents nuke.frame() to work properly inside of python panels (6.3v5) try: curPath = newStr % curFrame except: curPath = newStr exists = {True:' VALID PATH AT FRAME %s' % curFrame, False:' !!! PATH IS INVALID AT CURRENT FRAME (%s)!!!' % curFrame}[os.path.exists( curPath )] # BUILD INFO STRING infoStr2 += '%s.%s:\n\tbefore\t%s\n\tafter\t%s %s\n' % ( k.node().name(), k.name(), k.value(), newStr, exists ) self.info.setValue( '\n'.join( [infoStr1, infoStr2] ) ) return knobMatches def __NodeHasKnobWithName( self, node, name): try: node[name] except NameError: return False return True def __findNode( self, searchstr, knob ): v = knob.value() if not self.case.value(): v = v.lower() searchstr = searchstr.lower() if v and searchstr and searchstr in v: return True def __doSearch( self ): # LAUNCH SEARCH srcNodes = { 'all': nuke.allNodes(), 'selected': nuke.selectedNodes() } self.matches = self.getSearchResults( srcNodes[self.nodesChoice.value()] ) def __doReplace( self, knob ): # PERFORM REPLACE if self.case.value(): # CASE SENSITIVE newStr = re.sub( self.searchStr.value(), self.replaceStr.value(), knob.value() ) else: # IGNORE CASE newStr = knob.value() for m in re.findall( self.searchStr.value(), knob.value(), re.IGNORECASE ): newStr = re.sub( m, self.replaceStr.value(), newStr ) return newStr def knobChanged( self, knob ): if knob in ( self.searchStr, self.replaceStr, self.update, self.nodesChoice, self.case ): # PERFORM SEARCH self.__doSearch() elif knob is self.replace and self.matches is not None: # PERFORM REPLACE AND UPDATE HISTORY print("replacing") for k in self.matches: k.setValue( self.__doReplace( k ) ) self.updateHistory( self.searchStr.value(), self.replaceStr.value() ) self.history.setValues( self.loadHistory() ) self.history.setValue( 0 ) self.__doSearch() elif knob is self.history: search, replace = knob.value().split( self.delimiter ) self.searchStr.setValue( search ) self.replaceStr.setValue( replace ) self.__doSearch()