From 2e577274e63685c28f6fdef8cfded6adbcf63587 Mon Sep 17 00:00:00 2001 From: waaake Date: Mon, 28 Oct 2024 16:03:08 +0530 Subject: [PATCH] [ui] ScriptEditor: Added syntax colorization for the script editor Python syntax within the script editor is now highlighted making it easier to understand and write smaller code in it. --- meshroom/ui/components/__init__.py | 2 + meshroom/ui/components/scriptEditor.py | 178 ++++++++++++++++++- meshroom/ui/qml/GraphEditor/ScriptEditor.qml | 9 + 3 files changed, 187 insertions(+), 2 deletions(-) diff --git a/meshroom/ui/components/__init__.py b/meshroom/ui/components/__init__.py index aef07a61..0a2ebb8f 100755 --- a/meshroom/ui/components/__init__.py +++ b/meshroom/ui/components/__init__.py @@ -7,6 +7,7 @@ def registerTypes(): from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController, Transformations3DHelper from meshroom.ui.components.csvData import CsvData from meshroom.ui.components.geom2D import Geom2D + from meshroom.ui.components.scriptEditor import PySyntaxHighlighter qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea") qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable @@ -15,5 +16,6 @@ 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") qmlRegisterSingletonType(Geom2D, "Meshroom.Helpers", 1, 0, "Geom2D") diff --git a/meshroom/ui/components/scriptEditor.py b/meshroom/ui/components/scriptEditor.py index eb233d90..3bd1530b 100644 --- a/meshroom/ui/components/scriptEditor.py +++ b/meshroom/ui/components/scriptEditor.py @@ -1,9 +1,15 @@ -from PySide6.QtCore import QObject, Slot, QSettings - +""" Script Editor for Meshroom. +""" +# STD from io import StringIO from contextlib import redirect_stdout import traceback +# Qt +from PySide6 import QtCore, QtGui, QtQuick +from PySide6.QtCore import Property, QObject, Slot, Signal, QSettings + + class ScriptEditorManager(QObject): """ Manages the script editor history and logs. """ @@ -114,3 +120,171 @@ class ScriptEditorManager(QObject): 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) diff --git a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml index 7bc0eecb..1430613b 100644 --- a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml +++ b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml @@ -9,6 +9,8 @@ import Utils 1.0 import Qt.labs.platform 1.0 as Platform +import ScriptEditor 1.0 + Item { id: root @@ -339,6 +341,13 @@ Item { } } } + + // Syntax Highlights for the Input Area for Python Based Syntax + PySyntaxHighlighter { + id: syntaxHighlighter + // The document to highlight + textDocument: input.textDocument + } } } } \ No newline at end of file