123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- 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()
|