Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] Python Script Editor Improvements #2587

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions meshroom/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def registerTypes():
from meshroom.ui.components.filepath import FilepathHelper
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController, Transformations3DHelper
from meshroom.ui.components.csvData import CsvData
from meshroom.ui.components.scriptEditor import PySyntaxHighlighter

qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable
Expand All @@ -14,3 +15,4 @@ def registerTypes():
qmlRegisterType(Transformations3DHelper, "Meshroom.Helpers", 1, 0, "Transformations3DHelper") # TODO: uncreatable
qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController")
qmlRegisterType(CsvData, "DataObjects", 1, 0, "CsvData")
qmlRegisterType(PySyntaxHighlighter, "ScriptEditor", 1, 0, "PySyntaxHighlighter")
240 changes: 235 additions & 5 deletions meshroom/ui/components/scriptEditor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from PySide2.QtCore import QObject, Slot

""" Script Editor for Meshroom.
"""
# STD
from io import StringIO
from contextlib import redirect_stdout
import traceback

# Qt
from PySide2 import QtCore, QtGui, QtQuick
from PySide2.QtCore import Property, QObject, Slot, Signal, QSettings


class ScriptEditorManager(QObject):
""" Manages the script editor history and logs.
"""

_GROUP = "ScriptEditor"
_KEY = "script"

def __init__(self, parent=None):
super(ScriptEditorManager, self).__init__(parent=parent)
Expand All @@ -13,20 +25,52 @@ def __init__(self, parent=None):
self._globals = {}
self._locals = {}

# Protected
def _defaultScript(self):
""" Returns the default script for the script editor.
"""
lines = (
"from meshroom.ui import uiInstance\n",
"graph = uiInstance.activeProject.graph",
"for node in graph.nodes:",
" print(node.name)"
)

return "\n".join(lines)

def _lastScript(self):
""" Returns the last script from the user settings.
"""
settings = QSettings()
settings.beginGroup(self._GROUP)
return settings.value(self._KEY)

# Public
@Slot(str, result=str)
def process(self, script):
""" Execute the provided input script, capture the output from the standard output, and return it. """
# Saves the state if an exception has occured
exception = False

stdout = StringIO()
with redirect_stdout(stdout):
try:
exec(script, self._globals, self._locals)
except Exception as exception:
# Format and print the exception to stdout, which will be captured
print("{}: {}".format(type(exception).__name__, exception))
except Exception:
# Update that we have an exception that is thrown
exception = True
# Print the backtrace
traceback.print_exc(file=stdout)

result = stdout.getvalue().strip()

# Strip out additional part
if exception:
# We know that we're executing the above statement and that caused the exception
# What we want to show to the user is just the part that happened while executing the script
# So just split with the last part and show it to the user
result = result.split("self._locals)", 1)[-1]

# Add the script to the history and move up the index to the top of history stack
self._history.append(script)
self._index = len(self._history)
Expand Down Expand Up @@ -58,3 +102,189 @@ def getPreviousScript(self):
elif self._index == 0 and len(self._history):
return self._history[self._index]
return ""

@Slot(result=str)
def loadLastScript(self):
""" Returns the last executed script from the prefs.
"""
return self._lastScript() or self._defaultScript()

@Slot(str)
def saveScript(self, script):
""" Returns the last executed script from the prefs.

Args:
script (str): The script to save.
"""
settings = QSettings()
settings.beginGroup(self._GROUP)
settings.setValue(self._KEY, script)
settings.sync()


class CharFormat(QtGui.QTextCharFormat):
""" The Char format for the syntax.
"""

def __init__(self, color, bold=False, italic=False):
""" Constructor.
"""
super().__init__()

self._color = QtGui.QColor()
self._color.setNamedColor(color)

# Update the Foreground color
self.setForeground(self._color)

# The font characteristics
if bold:
self.setFontWeight(QtGui.QFont.Bold)
if italic:
self.setFontItalic(True)


class PySyntaxHighlighter(QtGui.QSyntaxHighlighter):
"""Syntax highlighter for the Python language.
"""

# Syntax styles that can be shared by all languages
STYLES = {
"keyword" : CharFormat("#9e59b3"), # Purple
"operator" : CharFormat("#2cb8a0"), # Teal
"brace" : CharFormat("#2f807e"), # Dark Aqua
"defclass" : CharFormat("#c9ba49", bold=True), # Yellow
"deffunc" : CharFormat("#4996c9", bold=True), # Blue
"string" : CharFormat("#7dbd39"), # Greeny
"comment" : CharFormat("#8d8d8d", italic=True), # Dark Grayish
"self" : CharFormat("#e6ba43", italic=True), # Yellow
"numbers" : CharFormat("#d47713"), # Orangish
}

# Python keywords
keywords = (
"and", "assert", "break", "class", "continue", "def",
"del", "elif", "else", "except", "exec", "finally",
"for", "from", "global", "if", "import", "in",
"is", "lambda", "not", "or", "pass", "print",
"raise", "return", "try", "while", "yield",
"None", "True", "False",
)

# Python operators
operators = (
"=",
# Comparison
"==", "!=", "<", "<=", ">", ">=",
# Arithmetic
r"\+", "-", r"\*", "/", "//", r"\%", r"\*\*",
# In-place
r"\+=", "-=", r"\*=", "/=", r"\%=",
# Bitwise
r"\^", r"\|", r"\&", r"\~", r">>", r"<<",
)

# Python braces
braces = (r"\{", r"\}", r"\(", r"\)", r"\[", r"\]")

def __init__(self, parent=None):
""" Constructor.

Keyword Args:
parent (QObject): The QObject parent from the QML side.
"""
super().__init__(parent)

# The Document to highlight
self._document = None

# Build a QRegExp for each of the pattern
self._rules = self.__rules()

# Private
def __rules(self):
""" Formatting rules.
"""
# Set of rules accordind to which the highlight should occur
rules = []

# Keyword rules
rules += [(QtCore.QRegExp(r"\b" + w + r"\s"), 0, PySyntaxHighlighter.STYLES["keyword"]) for w in PySyntaxHighlighter.keywords]
# Operator rules
rules += [(QtCore.QRegExp(o), 0, PySyntaxHighlighter.STYLES["operator"]) for o in PySyntaxHighlighter.operators]
# Braces
rules += [(QtCore.QRegExp(b), 0, PySyntaxHighlighter.STYLES["brace"]) for b in PySyntaxHighlighter.braces]

# All other rules
rules += [
# self
(QtCore.QRegExp(r'\bself\b'), 0, PySyntaxHighlighter.STYLES["self"]),

# 'def' followed by an identifier
(QtCore.QRegExp(r'\bdef\b\s*(\w+)'), 1, PySyntaxHighlighter.STYLES["deffunc"]),
# 'class' followed by an identifier
(QtCore.QRegExp(r'\bclass\b\s*(\w+)'), 1, PySyntaxHighlighter.STYLES["defclass"]),

# Numeric literals
(QtCore.QRegExp(r'\b[+-]?[0-9]+[lL]?\b'), 0, PySyntaxHighlighter.STYLES["numbers"]),
(QtCore.QRegExp(r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b'), 0, PySyntaxHighlighter.STYLES["numbers"]),
(QtCore.QRegExp(r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b'), 0, PySyntaxHighlighter.STYLES["numbers"]),

# Double-quoted string, possibly containing escape sequences
(QtCore.QRegExp(r'"[^"\\]*(\\.[^"\\]*)*"'), 0, PySyntaxHighlighter.STYLES["string"]),
# Single-quoted string, possibly containing escape sequences
(QtCore.QRegExp(r"'[^'\\]*(\\.[^'\\]*)*'"), 0, PySyntaxHighlighter.STYLES["string"]),

# From '#' until a newline
(QtCore.QRegExp(r'#[^\n]*'), 0, PySyntaxHighlighter.STYLES['comment']),
]

return rules

def highlightBlock(self, text):
""" Applies syntax highlighting to the given block of text.

Args:
text (str): The text to highlight.
"""
# Do other syntax formatting
for expression, nth, _format in self._rules:
# fetch the index of the expression in text
index = expression.indexIn(text, 0)

while index >= 0:
# We actually want the index of the nth match
index = expression.pos(nth)
length = len(expression.cap(nth))
self.setFormat(index, length, _format)
index = expression.indexIn(text, index + length)

def textDoc(self):
""" Returns the document being highlighted.
"""
return self._document

def setTextDocument(self, document):
""" Sets the document on the Highlighter.

Args:
document (QtQuick.QQuickTextDocument): The document from the QML engine.
"""
# If the same document is provided again
if document == self._document:
return

# Update the class document
self._document = document

# Set the document on the highlighter
self.setDocument(self._document.textDocument())

# Emit that the document is now changed
self.textDocumentChanged.emit()

# Signals
textDocumentChanged = Signal()

# Property
textDocument = Property(QtQuick.QQuickTextDocument, textDoc, setTextDocument, notify=textDocumentChanged)
Loading
Loading