
This tutorial will be a nice occasion to have a look at PyQt in a very light way, and how we can associate its strength with Maya, our aim is the following; the Command History of Maya is a bit sickly and pale so we're going to add some colors to make it more readable.
We'll first try to identify the scriptEditor's window, then we'll manage to get the correct QTextEdit child object, finally we'll assigne a QSyntaxHighlighter to it. Regular expressions basic knowledge may be useful to create your own coloring, let's start!
These two websites are great help if you want to learn more about regular expressions!So we're going to search the PyQt widget we want thank to the wrapinstance function from sip, aka wrapInstance for shiboken in completion with the wonderful class MQtUtil from OpenMayaUI in order to search inside Maya the widgets we are looking for. First let's find the Maya main window, which will be an easy shot;
The wrapInstance function needs a unique 'id' corresponding to the QWidget, and the main class of the widget, for our current example we're looking for the main window, so the class will be a QWidgetfrom PySide import QtCore, QtGui from shiboken import wrapInstance from maya.OpenMayaUI import MQtUtil maya_win = wrapInstance(long(MQtUtil.mainWindow()), QWidget)
Fine, now we can loop through children to find the scriptEditor window, several ways of approach can be used, we're just going to find all the children which contains 'script' in their object name, I'm pretty confident this will be the case for our script Editor window =)
for child in maya_win.children(): if 'script' in child.objectName(): print 'CHILD => %s' % child.objectName()
One shot! You should see something like that:
# CHILD => scriptEditorPanel1Window
We now have the name of our scriptEditor window and especially the corresponding object. Our second task will be to iterate inside its numerous children to find the QTextEdit of the History. The method is quite the same as above, except we need to do this recursively in all children and children's children to find all the QTextEdit instances to get the correct object's name. We spare you the pain of the search, so the name QTextEdit we're looking for is named cmdScrollFieldReporter1, take note that this widget have a unique name, and that a number is automatically added at the end. This means we could meet cmdScrollFieldReporter4.
Thank to the wonderful class MQtUtil we can now access to the widget using the function findControl
script_stdout = wrapInstance( long(MQtUtil.findControl('cmdScrollFieldReporter1')), QTextEdit )
Considering the fact that we don't now exactly the index of the widget's name we're going to use a little while loop to find the correct occurence:
i = 1 while i: try: se_edit = wrapInstance( long( MQtUtil.findControl('cmdScrollFieldReporter%i' % i)), QtGui.QTextEdit ) break except TypeError: i += 1
Alright! Now we have our widget we can work on the coloring
Applying the QSyntaxHighlighter can be achieved very simply be feeding him with a parent as first argument, like below:
class StdOut_Syntax(QSyntaxHighlighter): def __init__(self, parent): super(StdOut_Syntax, self).__init__(parent)
PyQt use the function highlightBlock to handle the syntax coloring, this function needs an input text - provided internally by the QTextEdit - in which we'll iterate for each regular expression we want to colorize, define a QTextCharFormat for the expression with the function setFormat, here is a simple example:
class StdOut_Syntax(QSyntaxHighlighter): def __init__(self, parent): super(StdOut_Syntax, self).__init__(parent) def highlightBlock(self, text): color = QColor(255, 125, 160) pattern = QRegExp(r'^//.+$') # regexp pattern keyword = QTextCharFormat() keyword.setForeground(color) # defining the aspect index = pattern.indexIn(text) while index >= 0: # loop through the text to find matches len = pattern.matchedLength() # length of the match # we apply the format to the match self.setFormat(index, len, keyword) index = pattern.indexIn(text, index + len) self.setCurrentBlockState(0)
The regular expression used above can be 'humanly translated' as:
Applying this class to our widget cmdsScrollFieldReporter with the following code we should get:
from PySide.QtGui import * from PySide.QtCore import * from shiboken import wrapInstance from maya.OpenMayaUI import MQtUtil class StdOut_Syntax(QSyntaxHighlighter): def __init__(self, parent): super(StdOut_Syntax, self).__init__(parent) self.parent = parent def highlightBlock(self, text): color = QColor(255, 125, 160) pattern = QRegExp(r'^//.+$') # regexp pattern keyword = QTextCharFormat() keyword.setForeground(color) # defining the aspect index = pattern.indexIn(text) while index >= 0: # loop through the text to find matches len = pattern.matchedLength() # length of the match # we apply the format to the match self.setFormat(index, len, keyword) index = pattern.indexIn(text, index + len) self.setCurrentBlockState(0) def wrap(): i = 1 while i: try: se_edit = wrapInstance( long( MQtUtil.findControl('cmdScrollFieldReporter%i'%i)), QTextEdit ) # we remove the old syntax and raise an exception to get # out of the while assert se_edit.findChild(QSyntaxHighlighter).deleteLater() except TypeError: i += 1 # if we don't find the widget we increment except (AttributeError, AssertionError): break return StdOut_Syntax(se_edit) wrap()
This means each time we'll meet the regular expression of "a line starting with // until the end of the line" will get a red color! Thus:
Now let's have a look how to "link" our QSyntaxHighlighter to Maya!
The execution of customized scripts when Maya starts can be easily achieved with the file userSetup.py in the folder Maya/scripts in your Documents, if the file doesn't exist, create it.
We will copy our awesome script in a new file named syntax.py in this folder, then we need to edit our userSetup.py file to add the following line:
import syntax
The use of the Maya's cmds function evalDeferred is often recommended if you want to differ the execution of scripts until Maya is 'available'.
Our example is a simple import statement so we don't need to do that =)!
Alrigh, then if you open your scriptEditor, and type
syntax.wrap()
our Command History QTextEdit will take some some colours!
Now we have our function which colorize the Command History of Maya, we need to link it to Maya so each time the scriptEditor opens, the QSyntaxHighlighter will be parented to the corresponding QTextEdit! Because everytime the window is closed, the Command History QTextEdit widget is deleted, so is our QSyntaxHighlighter!
If we turn on the option "Echo All Commands" in the scriptEditor we'll see that the command scriptEditor; is called when opening the window. After a search in Window → Settings/Preferences → Hotkey Editor under the Window section we find that the executed script for this function is:
if (`scriptedPanel -q -exists scriptEditorPanel1`) { scriptedPanel -e -to scriptEditorPanel1; showWindow scriptEditorPanel1Window; selectCurrentExecuterControl; }else { CommandWindow; }
Unfortunately these 'inside' functions in Maya aren't editable, and even if they were, this will only change the script for the keyboard shortcut, not the other ways to open the window, like the little button for instance, will continue to execute the function quoted above.
So we're going to have a look on the internal Maya files, with the secret hope to find this function in some .mel script, using a software like Notepad++we will be able to search inside all Maya's files and find the one which contains our command.
A little - and efficient - search indicates that the file defaultRunTimeCommands.mel in the folder scripts/startup inside the Maya folder contains what we are looking for.
The file is really huge, and the line we want differs between Maya versions, for Maya 2013 that line is the 4096th, for Maya 2014 that's the 4228th, simply search scriptEditor; in the file should bring you to correct line =)!
We just need to add a simple MEL command at the end of the Maya command, from:
-command ("if (`scriptedPanel -q -exists scriptEditorPanel1`) { scriptedPanel -e -to scriptEditorPanel1; showWindow scriptEditorPanel1Window; selectCurrentExecuterControl; }else { CommandWindow; }")
to
-command ("if (`scriptedPanel -q -exists scriptEditorPanel1`) { scriptedPanel -e -to scriptEditorPanel1; showWindow scriptEditorPanel1Window; selectCurrentExecuterControl; python(\"StdOut = syntax.wrap()\"); }else { CommandWindow; }")
So our color syntax will be called everytime scriptEditor; is executed, whatever how the command is called =)
Here is a bit more complex example to show you advanced use of the regular expressions
from PySide.QtGui import * from PySide.QtCore import * from shiboken import wrapInstance from maya.OpenMayaUI import MQtUtil class Rule(): def __init__(self, fg_color, pattern='', bg_color=None, bold=False, italic=False ): self.pattern = QRegExp(pattern) self.form = QTextCharFormat() self.form.setForeground(QColor(*fg_color)) if bg_color: self.form.setBackground(QColor(*bg_color)) font = QFont('Courier New', 9) font.setBold(bold) font.setItalic(italic) self.form.setFont(font) class StdOut_Syntax(QSyntaxHighlighter): def __init__(self, parent, rules): super(StdOut_Syntax, self).__init__(parent) self.parent = parent self.rules = rules def highlightBlock(self, text): # applying each rules for rule in self.rules: pattern = rule.pattern # regexp pattern index = pattern.indexIn(text) while index >= 0: # loop through the text to find matches len = pattern.matchedLength() # length of the match # we apply the format to the match self.setFormat(index, len, rule.form) index = pattern.indexIn(text, index + len) self.setCurrentBlockState(0) def wrap(): i = 1 while i: try: se_edit = wrapInstance(long( MQtUtil.findControl( 'cmdScrollFieldReporter%i'% i)), QTextEdit) # we remove the old syntax and raise an exception to get out of the while assert se_edit.findChild(QSyntaxHighlighter).deleteLater() except TypeError: i += 1 # if we don't find the widget we increment except (AttributeError, AssertionError): break rules = [Rule((212, 160, 125), r'^//.+$', bold=True), Rule((185, 125, 255), r'^#.+$', italic=True), Rule((255, 175, 44), r'^(#|//).*(error|Error).+$')] StdOut = StdOut_Syntax(se_edit, rules) return StdOut StdOut = wrap()
A simple translation of the above regular expressions would be:
Here is the result:
You can test new rules in real time with the two following lines: write your rules then copy them in your syntax.py to have them everytime you launch Maya!
StdOut.rules.append(Rule((255,255,255), r'^.*?Result.*?$', bold=True)) StdOut.rehighlight()
Will add a rule to colorize each line which contains the word Result in white bold
Here is a little example of a more advanced use of PyQt, this will assign directly the Maya's Python color syntax to the scriptEditor history, you'll just need to use what we've learned above to make it automatic! I believe in you =)
from PySide.QtCore import * from PySide.QtGui import * from shiboken import wrapInstance as wrapinstance from maya.OpenMayaUI import MQtUtil se_repo = wrapinstance(long(MQtUtil.findControl('cmdScrollFieldReporter1')), QTextEdit) tmp = cmds.cmdScrollFieldExecuter(sourceType='python') se_edit = wrapinstance(long(MQtUtil.findControl(tmp)), QTextEdit) se_edit.nativeParentWidget() se_edit.setVisible(False) high = se_edit.findChild(QSyntaxHighlighter) high.setDocument(se_repo.document())