Skip to content

Commit

Permalink
Merge pull request #1 from ldunham1/main
Browse files Browse the repository at this point in the history
PySide6 and teardown support.
  • Loading branch information
hannesdelbeke authored Sep 22, 2024
2 parents 7a7254f + a1dba70 commit 0071aea
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 119 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
This plugin lets you easily see errors and warnings in the Maya console.
This plugin lets you easily see errors, tracebacks, warnings and debug messages in the Maya console.

Improved from forks for performance, unloading and better matching.
Tested on Maya 2024+

![image](https://github.com/hannesdelbeke/maya_script_editor_highlighter/assets/3758308/4f15a866-1d87-45a5-9cf6-e779a4bf17ee)
it's quite primitive, e.g. the line `// https://www.autodesk.com/maya-arnold-not-available-error` will be colored as an error

### install
1. copy the script in your plugins folder `%UserProfile%\Documents\Maya\plug-ins`
2. enable the highlight plugin
3. open script editor.
4. it wont work untill you go out of focus, so click on the viewport then back on the editor.
### Install
1. Either;
a. Copy the script in your plugins folder `%UserProfile%\Documents\Maya\plug-ins`.
b. Use the supplied `script_editor_highlighter.mod` ([Maya module](https://help.autodesk.com/view/MAYAUL/2023/ENU/?guid=Maya_SDK_Distributing_Maya_Plug_ins_DistributingUsingModules_html))
2. Enable the plugin.
3. Open script editor.

> **_NOTE:_** It won't work until you go out of focus, so click on the viewport then back on the editor.
### reference
### Reference
see https://hannesdelbeke.github.io/wiki/tech%20art/maya/Maya%20script%20editor%20syntax%20highlight/
2 changes: 2 additions & 0 deletions script_editor_highlighter.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
+ script_editor_highlighter 3.0 ..
plug-ins: maya_script_editor_highlighter
248 changes: 137 additions & 111 deletions script_editor_highlighter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from maya import OpenMayaUI, cmds
import shiboken2
import keyword
import logging

from maya import OpenMayaUI, cmds
try:
from PySide2 import QtCore, QtGui, QtWidgets
from shiboken2 import wrapInstance
__QT_BINDINGS__ = 'PySide2'
except ImportError:
from PySide6 import QtCore, QtGui, QtWidgets
from shiboken6 import wrapInstance
__QT_BINDINGS__ = 'PySide6'


if __QT_BINDINGS__ == 'PySide6':
scriptEditorType = QtWidgets.QPlainTextEdit
else:
scriptEditorType = QtWidgets.QTextEdit


def maya_useNewAPI(): # noqa
Expand All @@ -15,147 +26,153 @@ def maya_useNewAPI(): # noqa
logger.setLevel(logging.INFO)


# TODO : finish implementing python keyword highlighting in stack traces
class PythonSyntaxRules(object):
keywords = [
"False",
"await",
"else",
"import",
"pass",
"None",
"break",
"except",
"in",
"raise",
"True",
"class",
"finally",
"is",
"return",
"and",
"continue",
"for",
"lambda",
"try",
"as",
"def",
"from",
"nonlocal",
"while",
"assert",
"del",
"global",
"not",
"with",
"async",
"elif",
"if",
"or",
"yield",
]
COLOURS = {
'default': QtGui.QColor(200, 200, 200),
'debug': QtGui.QColor(70, 200, 255),
'success': QtGui.QColor(16, 255, 8),
'warning': QtGui.QColor(255, 160, 0),
'error': QtGui.QColor(255, 0, 0),
'traceback': QtGui.QColor(150, 160, 255),
}

kOrange = QtGui.QColor(255, 80, 0)
# Modified word boundary to exclude dashes, underscores, dots and commas as boundaries.
MODIFIED_WORD_BOUNDARY = r'\b(?<!\-|\_|\.|\,)'

keyword_format = QtGui.QTextCharFormat()
keyword_format.setForeground(kOrange)

Rules = []
# For some reason this doesn't work as a list comprehension and yields an "undefined" for
# keyword format in the following list comprehension :
# Rules = [(QtCore.QRegExp(r"((\s){}(\s))".format(keyword)), keyword_format) for keyword in keywords]
for keyword in keywords:
Rules.append((QtCore.QRegExp(r"((\s){}(\s))".format(keyword)), keyword_format))
def get_rx_rule(name, pattern=None, case_sensitive=False):
# Use given pattern or use the name in a modified word boundary.
pattern = pattern or r'{0}{1}{0}'.format(MODIFIED_WORD_BOUNDARY, name)
rx_pattern = QtCore.QRegularExpression(pattern)
if not case_sensitive:
rx_pattern.setPatternOptions(QtCore.QRegularExpression.CaseInsensitiveOption)

rx_format = QtGui.QTextCharFormat()
rx_format.setForeground(COLOURS[name])
return rx_pattern, rx_format

class StdOut_Syntax(QtGui.QSyntaxHighlighter):

kWhite = QtGui.QColor(200, 200, 200)
kRed = QtGui.QColor(255, 0, 0)
kOrange = QtGui.QColor(255, 160, 0)
kGreen = QtGui.QColor(16, 255, 8)
kBlue = QtGui.QColor(35, 160, 255)
kLightBlue = QtGui.QColor(70, 200, 255)

rx_error = QtCore.QRegExp(r"[Ee][Rr][Rr][Oo][Rr]")
error_format = QtGui.QTextCharFormat()
error_format.setForeground(kRed)
# TODO : finish implementing python keyword highlighting in stack traces
class PythonSyntaxRules(object):

rx_warning = QtCore.QRegExp(r"[Ww][Aa][Rr][Nn][Ii][Nn][Gg]")
warning_format = QtGui.QTextCharFormat()
warning_format.setForeground(kOrange)
keyword_format = QtGui.QTextCharFormat()
keyword_format.setFontWeight(QtGui.QFont.Bold)
keyword_format.setForeground(COLOURS['warning'])

rx_debug = QtCore.QRegExp(r"[Dd][Ee][Bb][Uu][Gg]")
debug_format = QtGui.QTextCharFormat()
debug_format.setForeground(kLightBlue)
# For some reason this doesn't work as a list comprehension and yields an "undefined" for
# keyword format in the following list comprehension :
# Rules = [(QRegularExpression(r"((\s){}(\s))".format(keyword)), keyword_format) for keyword in keywords]
# We also here just put all matching works into a single pattern for performance.
Rules = [
(
QtCore.QRegularExpression(r'{0}(?:{1}){0}'.format(MODIFIED_WORD_BOUNDARY, '|'.join(keyword.kwlist))),
keyword_format
)
]

rx_debug = QtCore.QRegExp(r"[Ss][Uu][Cc][Cc][Ee][Ss]")
debug_format = QtGui.QTextCharFormat()
debug_format.setForeground(kGreen)

rx_traceback_start = QtCore.QRegExp("Traceback \(most recent call last\)")
traceback_format = QtGui.QTextCharFormat()
traceback_format.setForeground(kBlue)
class StdOut_Syntax(QtGui.QSyntaxHighlighter):

default_format = QtGui.QTextCharFormat()
default_format.setForeground(kWhite)
default_format.setForeground(COLOURS['default'])

rx_traceback_start, traceback_format = get_rx_rule(
'traceback',
r'Traceback \(most recent call last\)\:',
case_sensitive=True,
)

Rules = [
(rx_debug, debug_format),
(rx_warning, warning_format),
(rx_error, error_format),
get_rx_rule('error'),
get_rx_rule('warning'),
get_rx_rule('success'),
get_rx_rule('debug'),
]

def __init__(self, parent):
super(StdOut_Syntax, self).__init__(parent)
self.parent = parent
self.normal, self.traceback = range(2)
BlockState_Normal = 0
BlockState_Traceback = 1

def highlightBlock(self, t):
self.setCurrentBlockState(self.normal)
if self.isTraceback(t):
self.setFormat(0, len(t), self.traceback_format)
self.setCurrentBlockState(self.traceback)
@staticmethod
def __pattern_match(text, rx_pattern):
if __QT_BINDINGS__ == 'PySide6':
match = rx_pattern.match(text).hasMatch()
else:
self.line_formatting(t)

def line_formatting(self, t):
for regex, formatting in StdOut_Syntax.Rules:
i = regex.indexIn(t)
if i > 0:
self.setFormat(0, len(t), formatting)

def isTraceback(self, t):
return (
self.previousBlockState() == self.normal
and self.rx_traceback_start.indexIn(t) > 0
) or (self.previousBlockState() == self.traceback and t.startswith("# "))
match = rx_pattern.indexIn(text)
return bool(match)

def highlightBlock(self, line):
self.setCurrentBlockState(self.BlockState_Normal)
if self.isTraceback(line):
self.setFormat(0, len(line), self.traceback_format)
self.setCurrentBlockState(self.BlockState_Traceback)
else:
self.lineFormatting(line)

def lineFormatting(self, line):
for rx_pattern, formatting in self.Rules:
if self.__pattern_match(line, rx_pattern):
self.setFormat(0, len(line), formatting)
break

def isTraceback(self, line):
previousBlockState = self.previousBlockState()
if (previousBlockState == self.BlockState_Normal and
self.__pattern_match(line, self.rx_traceback_start)):
return True
elif (previousBlockState == self.BlockState_Traceback and
line.startswith("# ") and
not any(self.__pattern_match(line, rule[0]) for rule in self.Rules)):
return True
return False


def __se_highlight():
logger.debug("Attaching highlighter")
i = 1
while True:
script_editor_output_name = "cmdScrollFieldReporter{0}".format(i)
script_editor_output_name = "cmdScrollFieldReporter{}".format(i)
script_editor_output_object = OpenMayaUI.MQtUtil.findControl(script_editor_output_name)
if not script_editor_output_object:
break

script_editor_output_widget = wrapInstance(int(script_editor_output_object), scriptEditorType)
logger.debug(script_editor_output_widget)
document = script_editor_output_widget.document()
if not document.findChild(StdOut_Syntax):
StdOut_Syntax(document)
logger.debug("Done attaching highlighter to : %s", script_editor_output_widget)
i += 1


def __se_remove_highlight():
logger.debug('Detaching highlighter')
i = 1
while True:
script_editor_output_name = "cmdScrollFieldReporter{}".format(i)
script_editor_output_object = OpenMayaUI.MQtUtil.findControl(script_editor_output_name)
if not script_editor_output_object:
break
script_editor_output_widget = shiboken2.wrapInstance(int(script_editor_output_object), QtWidgets.QTextEdit)

script_editor_output_widget = wrapInstance(int(script_editor_output_object), scriptEditorType)
logger.debug(script_editor_output_widget)
StdOut_Syntax(script_editor_output_widget.document())
logger.debug("Done attaching highlighter to : %s" % script_editor_output_widget)
document = script_editor_output_widget.document()
highlighter = document.findChild(StdOut_Syntax)
if highlighter:
highlighter.setParent(None)
highlighter.deleteLater()
logger.debug("Done detaching highlighter from : %s", script_editor_output_widget)
i += 1


__qt_focus_change_callback = {"cmdScrollFieldReporter": __se_highlight}


def __on_focus_changed(old_widget, new_widget):
if new_widget:
widget_name = new_widget.objectName()
for callback in [
name for name in __qt_focus_change_callback if widget_name.startswith(name)
]:
if not new_widget:
return

widgetName = new_widget.objectName()
for callback in __qt_focus_change_callback:
if widgetName.startswith(callback):
__qt_focus_change_callback[callback]()


Expand All @@ -164,13 +181,22 @@ def setup_highlighter():
app = QtWidgets.QApplication.instance()
app.focusChanged.connect(__on_focus_changed)
__se_highlight()
except Exception as exp:
pass
except Exception as e:
logger.warning('Failed to setup highlighter: %s', e, exc_info=True)


def teardown_highlighter():
try:
__se_remove_highlight()
app = QtWidgets.QApplication.instance()
app.focusChanged.disconnect(__on_focus_changed)
except Exception as e:
logger.warning('Failed to teardown highlighter: %s', e, exc_info=True)


def initializePlugin(plugin):
cmds.evalDeferred(setup_highlighter)


def uninitializePlugin(plugin):
...# todo
teardown_highlighter()

0 comments on commit 0071aea

Please sign in to comment.