1- from PySide6 .QtCore import QObject , Slot
2-
1+ """ Script Editor for Meshroom.
2+ """
3+ # STD
34from io import StringIO
45from contextlib import redirect_stdout
6+ import traceback
7+
8+ # Qt
9+ from PySide6 import QtCore , QtGui
10+ from PySide6 .QtCore import Property , QObject , Slot , Signal , QSettings
11+
512
613class ScriptEditorManager (QObject ):
14+ """ Manages the script editor history and logs.
15+ """
16+
17+ _GROUP = "ScriptEditor"
18+ _KEY = "script"
719
820 def __init__ (self , parent = None ):
921 super (ScriptEditorManager , self ).__init__ (parent = parent )
@@ -13,23 +25,68 @@ def __init__(self, parent=None):
1325 self ._globals = {}
1426 self ._locals = {}
1527
28+ # Protected
29+ def _defaultScript (self ):
30+ """ Returns the default script for the script editor.
31+ """
32+ lines = (
33+ "from meshroom.ui import uiInstance\n " ,
34+ "graph = uiInstance.activeProject.graph" ,
35+ "for node in graph.nodes:" ,
36+ " print(node.name)"
37+ )
1638
39+ return "\n " .join (lines )
40+
41+ def _lastScript (self ):
42+ """ Returns the last script from the user settings.
43+ """
44+ settings = QSettings ()
45+ settings .beginGroup (self ._GROUP )
46+ return settings .value (self ._KEY )
47+
48+ def _hasPreviousScript (self ):
49+ """ Returns whether there is a previous script available.
50+ """
51+ # If the current index is greater than the first
52+ return self ._index > 0
53+
54+ def _hasNextScript (self ):
55+ """ Returns whethere there is a new script available to load.
56+ """
57+ # If the current index is lower than the available indexes
58+ return self ._index < (len (self ._history ) - 1 )
59+
60+ # Public
1761 @Slot (str , result = str )
1862 def process (self , script ):
1963 """ Execute the provided input script, capture the output from the standard output, and return it. """
64+ # Saves the state if an exception has occured
65+ exception = False
66+
2067 stdout = StringIO ()
2168 with redirect_stdout (stdout ):
2269 try :
2370 exec (script , self ._globals , self ._locals )
24- except Exception as exception :
25- # Format and print the exception to stdout, which will be captured
26- print ("{}: {}" .format (type (exception ).__name__ , exception ))
71+ except Exception :
72+ # Update that we have an exception that is thrown
73+ exception = True
74+ # Print the backtrace
75+ traceback .print_exc (file = stdout )
2776
2877 result = stdout .getvalue ().strip ()
2978
79+ # Strip out additional part
80+ if exception :
81+ # We know that we're executing the above statement and that caused the exception
82+ # What we want to show to the user is just the part that happened while executing the script
83+ # So just split with the last part and show it to the user
84+ result = result .split ("self._locals)" , 1 )[- 1 ]
85+
3086 # Add the script to the history and move up the index to the top of history stack
3187 self ._history .append (script )
3288 self ._index = len (self ._history )
89+ self .scriptIndexChanged .emit ()
3390
3491 return result
3592
@@ -45,6 +102,7 @@ def getNextScript(self):
45102 If there is no next entry, return an empty string. """
46103 if self ._index + 1 < len (self ._history ) and len (self ._history ) > 0 :
47104 self ._index = self ._index + 1
105+ self .scriptIndexChanged .emit ()
48106 return self ._history [self ._index ]
49107 return ""
50108
@@ -54,7 +112,202 @@ def getPreviousScript(self):
54112 If there is no previous entry, return an empty string. """
55113 if self ._index - 1 >= 0 and self ._index - 1 < len (self ._history ):
56114 self ._index = self ._index - 1
115+ self .scriptIndexChanged .emit ()
57116 return self ._history [self ._index ]
58117 elif self ._index == 0 and len (self ._history ):
59118 return self ._history [self ._index ]
60119 return ""
120+
121+ @Slot (result = str )
122+ def loadLastScript (self ):
123+ """ Returns the last executed script from the prefs.
124+ """
125+ return self ._lastScript () or self ._defaultScript ()
126+
127+ @Slot (str )
128+ def saveScript (self , script ):
129+ """ Returns the last executed script from the prefs.
130+
131+ Args:
132+ script (str): The script to save.
133+ """
134+ settings = QSettings ()
135+ settings .beginGroup (self ._GROUP )
136+ settings .setValue (self ._KEY , script )
137+ settings .sync ()
138+
139+ scriptIndexChanged = Signal ()
140+
141+ hasPreviousScript = Property (bool , _hasPreviousScript , notify = scriptIndexChanged )
142+ hasNextScript = Property (bool , _hasNextScript , notify = scriptIndexChanged )
143+
144+
145+ class CharFormat (QtGui .QTextCharFormat ):
146+ """ The Char format for the syntax.
147+ """
148+
149+ def __init__ (self , color , bold = False , italic = False ):
150+ """ Constructor.
151+ """
152+ super ().__init__ ()
153+
154+ self ._color = QtGui .QColor ()
155+ self ._color .setNamedColor (color )
156+
157+ # Update the Foreground color
158+ self .setForeground (self ._color )
159+
160+ # The font characteristics
161+ if bold :
162+ self .setFontWeight (QtGui .QFont .Bold )
163+ if italic :
164+ self .setFontItalic (True )
165+
166+
167+ class PySyntaxHighlighter (QtGui .QSyntaxHighlighter ):
168+ """Syntax highlighter for the Python language.
169+ """
170+
171+ # Syntax styles that can be shared by all languages
172+ STYLES = {
173+ "keyword" : CharFormat ("#9e59b3" ), # Purple
174+ "operator" : CharFormat ("#2cb8a0" ), # Teal
175+ "brace" : CharFormat ("#2f807e" ), # Dark Aqua
176+ "defclass" : CharFormat ("#c9ba49" , bold = True ), # Yellow
177+ "deffunc" : CharFormat ("#4996c9" , bold = True ), # Blue
178+ "string" : CharFormat ("#7dbd39" ), # Greeny
179+ "comment" : CharFormat ("#8d8d8d" , italic = True ), # Dark Grayish
180+ "self" : CharFormat ("#e6ba43" , italic = True ), # Yellow
181+ "numbers" : CharFormat ("#d47713" ), # Orangish
182+ }
183+
184+ # Python keywords
185+ keywords = (
186+ "and" , "assert" , "break" , "class" , "continue" , "def" ,
187+ "del" , "elif" , "else" , "except" , "exec" , "finally" ,
188+ "for" , "from" , "global" , "if" , "import" , "in" ,
189+ "is" , "lambda" , "not" , "or" , "pass" , "print" ,
190+ "raise" , "return" , "try" , "while" , "yield" ,
191+ "None" , "True" , "False" ,
192+ )
193+
194+ # Python operators
195+ operators = (
196+ "=" ,
197+ # Comparison
198+ "==" , "!=" , "<" , "<=" , ">" , ">=" ,
199+ # Arithmetic
200+ r"\+" , "-" , r"\*" , "/" , "//" , r"\%" , r"\*\*" ,
201+ # In-place
202+ r"\+=" , "-=" , r"\*=" , "/=" , r"\%=" ,
203+ # Bitwise
204+ r"\^" , r"\|" , r"\&" , r"\~" , r">>" , r"<<" ,
205+ )
206+
207+ # Python braces
208+ braces = (r"\{" , r"\}" , r"\(" , r"\)" , r"\[" , r"\]" )
209+
210+ def __init__ (self , parent = None ):
211+ """ Constructor.
212+
213+ Keyword Args:
214+ parent (QObject): The QObject parent from the QML side.
215+ """
216+ super ().__init__ (parent )
217+
218+ # The Document to highlight
219+ self ._document = None
220+
221+ # Build a QRegularExpression for each of the pattern
222+ self ._rules = self .__rules ()
223+
224+ # Private
225+ def __rules (self ):
226+ """ Formatting rules.
227+ """
228+ # Set of rules accordind to which the highlight should occur
229+ rules = []
230+
231+ # Keyword rules
232+ rules += [(QtCore .QRegularExpression (r"\b" + w + r"\s" ), 0 , PySyntaxHighlighter .STYLES ["keyword" ]) for w in PySyntaxHighlighter .keywords ]
233+ # Operator rules
234+ rules += [(QtCore .QRegularExpression (o ), 0 , PySyntaxHighlighter .STYLES ["operator" ]) for o in PySyntaxHighlighter .operators ]
235+ # Braces
236+ rules += [(QtCore .QRegularExpression (b ), 0 , PySyntaxHighlighter .STYLES ["brace" ]) for b in PySyntaxHighlighter .braces ]
237+
238+ # All other rules
239+ rules += [
240+ # self
241+ (QtCore .QRegularExpression (r'\bself\b' ), 0 , PySyntaxHighlighter .STYLES ["self" ]),
242+
243+ # 'def' followed by an identifier
244+ (QtCore .QRegularExpression (r'\bdef\b\s*(\w+)' ), 1 , PySyntaxHighlighter .STYLES ["deffunc" ]),
245+ # 'class' followed by an identifier
246+ (QtCore .QRegularExpression (r'\bclass\b\s*(\w+)' ), 1 , PySyntaxHighlighter .STYLES ["defclass" ]),
247+
248+ # Numeric literals
249+ (QtCore .QRegularExpression (r'\b[+-]?[0-9]+[lL]?\b' ), 0 , PySyntaxHighlighter .STYLES ["numbers" ]),
250+ (QtCore .QRegularExpression (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b' ), 0 , PySyntaxHighlighter .STYLES ["numbers" ]),
251+ (QtCore .QRegularExpression (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b' ), 0 , PySyntaxHighlighter .STYLES ["numbers" ]),
252+
253+ # Double-quoted string, possibly containing escape sequences
254+ (QtCore .QRegularExpression (r'"[^"\\]*(\\.[^"\\]*)*"' ), 0 , PySyntaxHighlighter .STYLES ["string" ]),
255+ # Single-quoted string, possibly containing escape sequences
256+ (QtCore .QRegularExpression (r"'[^'\\]*(\\.[^'\\]*)*'" ), 0 , PySyntaxHighlighter .STYLES ["string" ]),
257+
258+ # From '#' until a newline
259+ (QtCore .QRegularExpression (r'#[^\n]*' ), 0 , PySyntaxHighlighter .STYLES ['comment' ]),
260+ ]
261+
262+ return rules
263+
264+ def highlightBlock (self , text ):
265+ """ Applies syntax highlighting to the given block of text.
266+
267+ Args:
268+ text (str): The text to highlight.
269+ """
270+ # Do other syntax formatting
271+ for expression , nth , _format in self ._rules :
272+ # fetch the index of the expression in text
273+ match = expression .match (text , 0 )
274+ index = match .capturedStart ()
275+
276+ while index >= 0 :
277+ # We actually want the index of the nth match
278+ index = match .capturedStart (nth )
279+ length = len (match .captured (nth ))
280+ self .setFormat (index , length , _format )
281+ # index = expression.indexIn(text, index + length)
282+ match = expression .match (text , index + length )
283+ index = match .capturedStart ()
284+
285+ def textDoc (self ):
286+ """ Returns the document being highlighted.
287+ """
288+ return self ._document
289+
290+ def setTextDocument (self , document ):
291+ """ Sets the document on the Highlighter.
292+
293+ Args:
294+ document (QtQuick.QQuickTextDocument): The document from the QML engine.
295+ """
296+ # If the same document is provided again
297+ if document == self ._document :
298+ return
299+
300+ # Update the class document
301+ self ._document = document
302+
303+ # Set the document on the highlighter
304+ self .setDocument (self ._document .textDocument ())
305+
306+ # Emit that the document is now changed
307+ self .textDocumentChanged .emit ()
308+
309+ # Signals
310+ textDocumentChanged = Signal ()
311+
312+ # Property
313+ textDocument = Property (QObject , textDoc , setTextDocument , notify = textDocumentChanged )
0 commit comments