Vincent Touache - Character TD
/tutos/10_scriptEditor_color/content.php
data=================:ArrayColor syntax the scriptEditor History

Table of Contents

  1. Introduction
  2. PyQt
    1. Identify our aim
    2. The QSyntaxHighlighter
  3. Linking intrinsically to Maya
    1. Starting at launch
    2. A bit of hacking =)!
    3. Finalizing
  4. Bonus



Introduction

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!

PyQt

Identify our aim

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 QWidget
  from 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

The QSyntaxHighlighter

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:

A QTextEdit accepts only one QSyntaxHighlighter associated, if a second one is linked to the QTextEdit, result may be unpredictable, so the previous QSyntaxHighlighter must be removed from the QTextEdit children everytime you want to refresh it completely!

Now let's have a look how to "link" our QSyntaxHighlighter to Maya!

Linking intrinsically to Maya

Starting at launch

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!

A bit of hacking =)!

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 =)

Finalizing

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:

Coloring the QTextEdit

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 the final file

Bonus

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