1
- from PySide6 .QtCore import QObject , Slot
2
-
1
+ """ Script Editor for Meshroom.
2
+ """
3
+ # STD
3
4
from io import StringIO
4
5
from 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
+
5
12
6
13
class ScriptEditorManager (QObject ):
14
+ """ Manages the script editor history and logs.
15
+ """
16
+
17
+ _GROUP = "ScriptEditor"
18
+ _KEY = "script"
7
19
8
20
def __init__ (self , parent = None ):
9
21
super (ScriptEditorManager , self ).__init__ (parent = parent )
@@ -13,23 +25,68 @@ def __init__(self, parent=None):
13
25
self ._globals = {}
14
26
self ._locals = {}
15
27
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
+ )
16
38
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
17
61
@Slot (str , result = str )
18
62
def process (self , script ):
19
63
""" 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
+
20
67
stdout = StringIO ()
21
68
with redirect_stdout (stdout ):
22
69
try :
23
70
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 )
27
76
28
77
result = stdout .getvalue ().strip ()
29
78
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
+
30
86
# Add the script to the history and move up the index to the top of history stack
31
87
self ._history .append (script )
32
88
self ._index = len (self ._history )
89
+ self .scriptIndexChanged .emit ()
33
90
34
91
return result
35
92
@@ -45,6 +102,7 @@ def getNextScript(self):
45
102
If there is no next entry, return an empty string. """
46
103
if self ._index + 1 < len (self ._history ) and len (self ._history ) > 0 :
47
104
self ._index = self ._index + 1
105
+ self .scriptIndexChanged .emit ()
48
106
return self ._history [self ._index ]
49
107
return ""
50
108
@@ -54,7 +112,202 @@ def getPreviousScript(self):
54
112
If there is no previous entry, return an empty string. """
55
113
if self ._index - 1 >= 0 and self ._index - 1 < len (self ._history ):
56
114
self ._index = self ._index - 1
115
+ self .scriptIndexChanged .emit ()
57
116
return self ._history [self ._index ]
58
117
elif self ._index == 0 and len (self ._history ):
59
118
return self ._history [self ._index ]
60
119
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