From 8984aac20e2b9dcb5ce1d99d965f6caca1f8caf2 Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Mon, 28 Oct 2024 14:52:29 +0530
Subject: [PATCH 01/10] [ui] ScriptEditor: Updated Core ScriptEditor Manager to
 show better exceptions

ScriptEditorManager now also allows the code to be saved and retrieved back. Exceptions are now shown with a better output to the user.
---
 meshroom/ui/components/scriptEditor.py | 64 ++++++++++++++++++++++++--
 1 file changed, 60 insertions(+), 4 deletions(-)

diff --git a/meshroom/ui/components/scriptEditor.py b/meshroom/ui/components/scriptEditor.py
index da3731aded..eb233d90ad 100644
--- a/meshroom/ui/components/scriptEditor.py
+++ b/meshroom/ui/components/scriptEditor.py
@@ -1,9 +1,15 @@
-from PySide6.QtCore import QObject, Slot
+from PySide6.QtCore import QObject, Slot, QSettings
 
 from io import StringIO
 from contextlib import redirect_stdout
+import traceback
 
 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)
@@ -13,20 +19,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)
@@ -58,3 +96,21 @@ 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()

From 4464cdf7991d895ed89d7d643508c5fcafb7d1cc Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Mon, 28 Oct 2024 15:48:26 +0530
Subject: [PATCH 02/10] [ui] ScriptEditor: Updated Script Editor layout

ScriptEditor is now part of a ColumnLayout in an MSplitView allowing more control over what is being viewed.
---
 meshroom/ui/qml/GraphEditor/ScriptEditor.qml | 167 ++++++++++---------
 1 file changed, 90 insertions(+), 77 deletions(-)

diff --git a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
index 933299e92f..7bc0eecb32 100644
--- a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
+++ b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
@@ -12,19 +12,49 @@ import Qt.labs.platform 1.0 as Platform
 Item {
     id: root
 
+    function replace(text, string, replacement) {
+        /*
+         * Replaces all occurences of the string in the text
+         * @param text - overall text
+         * @param string - the string to be replaced in the text
+         * @param replacement - the replacement of the string
+         */
+        // Split with the string
+        let lines = text.split(string)
+        // Return the overall text joined with the replacement
+        return lines.join(replacement)
+    }
+
     function formatInput(text) {
-        var lines = text.split("\n")
-        for (let i = 0; i < lines.length; ++i) {
-            lines[i] = ">>> " + lines[i]
-        }
-        return lines.join("\n")
+        /*
+         * Formats the text to be displayed as the input script executed
+         */
+
+        // Replace the text to be RichText Supportive
+        return "<font color=#868686>" + replace(text, "\n", "<br>") + "</font><br><br>"
+    }
+
+    function formatOutput(text) {
+        /*
+         * Formats the text to be displayed as the result of the script executed
+         */
+
+        // Replace the text to be RichText Supportive
+        return "<font color=#49a1f3>" + "Result: " + replace(text, "\n", "<br>") + "</font><br><br>"
     }
 
-    function processScript() {
-        output.clear()
-        var ret = ScriptEditorManager.process(input.text)
-        output.text = formatInput(input.text) + "\n\n" + ret
-        input.clear()
+    function processScript(text = "") {
+        // Use either the provided/selected or the entire script
+        text = text || input.text
+
+        // Execute the process and fetch back the return for it
+        var ret = ScriptEditorManager.process(text)
+
+        // Append the input script and the output result to the output console
+        output.append(formatInput(text) + formatOutput(ret))
+
+        // Save the entire script after executing the commands
+        ScriptEditorManager.saveScript(input.text)
     }
 
     function loadScript(fileUrl) {
@@ -83,12 +113,8 @@ Item {
         RowLayout {
             Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
 
-            Item {
-                Layout.fillWidth: true
-            }
-
             MaterialToolButton {
-                font.pointSize: 13
+                font.pointSize: 18
                 text: MaterialIcons.download
                 ToolTip.text: "Load Script"
 
@@ -98,7 +124,7 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 13
+                font.pointSize: 18
                 text: MaterialIcons.upload
                 ToolTip.text: "Save Script"
 
@@ -113,7 +139,7 @@ Item {
 
             MaterialToolButton {
                 id: executeButton
-                font.pointSize: 13
+                font.pointSize: 18
                 text: MaterialIcons.slideshow
                 ToolTip.text: "Execute Script"
 
@@ -123,7 +149,7 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 13
+                font.pointSize: 18
                 text: MaterialIcons.cancel_presentation
                 ToolTip.text: "Clear Output Window"
 
@@ -137,7 +163,7 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 13
+                font.pointSize: 18
                 text: MaterialIcons.history
                 ToolTip.text: "Get Previous Script"
 
@@ -152,7 +178,7 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 13
+                font.pointSize: 18
                 text: MaterialIcons.update
                 ToolTip.text: "Get Next Script"
 
@@ -167,7 +193,7 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 13
+                font.pointSize: 18
                 text: MaterialIcons.backspace
                 ToolTip.text: "Clear History"
 
@@ -183,31 +209,50 @@ Item {
             }
         }
 
-        RowLayout {
-            Label {
-                text: "Input"
-                font.bold: true
-                horizontalAlignment: Text.AlignHCenter
-                Layout.fillWidth: true
-            }
+        MSplitView {
+            id: topBottomSplit
+            Layout.fillHeight: true
+            Layout.fillWidth: true
 
-            Label {
-                text: "Output"
-                font.bold: true
-                horizontalAlignment: Text.AlignHCenter
-                Layout.fillWidth: true
-            }
-        }
+            orientation: Qt.Vertical
+        
+            // Output Text Area -- Shows the output for the executed script(s)
+            Rectangle {
+                id: outputArea
 
-        RowLayout {
-            Layout.fillWidth: true
-            Layout.fillHeight: true
-            width: root.width
+                // Has a minimum height
+                SplitView.minimumHeight: 80
+
+                color: palette.base
+
+                Flickable {
+                    width: parent.width
+                    height: parent.height
+                    contentWidth: width
+                    contentHeight: ( output.lineCount + 5 ) * output.font.pixelSize // + 5 lines for buffer to be scrolled and visibility
+
+                    ScrollBar.vertical: MScrollBar {}
+
+                    TextArea.flickable: TextArea {
+                        id: output
 
+                        readOnly: true
+                        selectByMouse: true
+                        padding: 0
+                        Layout.fillHeight: true
+                        Layout.fillWidth: true
+                        wrapMode: Text.WordWrap
+
+                        textFormat: Text.RichText
+                    }
+                }
+            }
+
+            // Input Text Area -- Holds the input scripts to be executed
             Rectangle {
                 id: inputArea
-                Layout.fillHeight: true
-                Layout.fillWidth: true
+
+                SplitView.fillHeight: true
 
                 color: palette.base
 
@@ -254,7 +299,7 @@ Item {
                     width: parent.width
                     height: parent.height
                     contentWidth: width
-                    contentHeight: height
+                    contentHeight: ( input.lineCount + 5 ) * input.font.pixelSize // + 5 lines for buffer to be scrolled and visibility
 
                     anchors.left: lineNumbers.right
                     anchors.top: parent.top
@@ -266,13 +311,8 @@ Item {
                     TextArea.flickable: TextArea {
                         id: input
 
-                        text: {
-                            var str = "from meshroom.ui import uiInstance\n\n"
-                            str += "graph = uiInstance.activeProject.graph\n"
-                            str += "for node in graph.nodes:\n"
-                            str += "    print(node.name)"
-                            return str
-                        }
+                        text: ScriptEditorManager.loadLastScript()
+
                         font: lineNumbers.textMetrics.font
                         Layout.fillHeight: true
                         Layout.fillWidth: true
@@ -287,7 +327,7 @@ Item {
 
                         Keys.onPressed: function(event) {
                             if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return) && event.modifiers === Qt.ControlModifier) {
-                                root.processScript()
+                                root.processScript(input.selectedText)
                             }
                         }
                     }
@@ -299,33 +339,6 @@ Item {
                     }
                 }
             }
-            
-            Rectangle {
-                id: outputArea
-                Layout.fillHeight: true
-                Layout.fillWidth: true
-
-                color: palette.base
-
-                Flickable {
-                    width: parent.width
-                    height: parent.height
-                    contentWidth: width
-                    contentHeight: height
-
-                    ScrollBar.vertical: MScrollBar {}
-
-                    TextArea.flickable: TextArea {
-                        id: output
-
-                        readOnly: true
-                        selectByMouse: true
-                        padding: 0
-                        Layout.fillHeight: true
-                        Layout.fillWidth: true
-                    }
-                }
-            }
         }
     }
 }
\ No newline at end of file

From 2e577274e63685c28f6fdef8cfded6adbcf63587 Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Mon, 28 Oct 2024 16:03:08 +0530
Subject: [PATCH 03/10] [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 aef07a61f2..0a2ebb8f04 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 eb233d90ad..3bd1530b94 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 @@ def saveScript(self, script):
         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 7bc0eecb32..1430613b56 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

From 07309361ad9b767198a5fbd26d06a8e25cb3bc80 Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Tue, 26 Nov 2024 15:45:55 +0100
Subject: [PATCH 04/10] [ui] Qt6 Compatibility for ScriptEditor: Updated
 QRegexp -> QRegularExpression

---
 meshroom/ui/components/scriptEditor.py | 41 ++++++++++++++------------
 1 file changed, 22 insertions(+), 19 deletions(-)

diff --git a/meshroom/ui/components/scriptEditor.py b/meshroom/ui/components/scriptEditor.py
index 3bd1530b94..8ef82edcd0 100644
--- a/meshroom/ui/components/scriptEditor.py
+++ b/meshroom/ui/components/scriptEditor.py
@@ -6,7 +6,7 @@
 import traceback
 
 # Qt
-from PySide6 import QtCore, QtGui, QtQuick
+from PySide6 import QtCore, QtGui
 from PySide6.QtCore import Property, QObject, Slot, Signal, QSettings
 
 
@@ -198,7 +198,7 @@ def __init__(self, parent=None):
         # The Document to highlight
         self._document = None
 
-        # Build a QRegExp for each of the pattern
+        # Build a QRegularExpression for each of the pattern
         self._rules = self.__rules()
 
     # Private
@@ -209,34 +209,34 @@ def __rules(self):
         rules = []
 
         # Keyword rules
-        rules += [(QtCore.QRegExp(r"\b" + w + r"\s"), 0, PySyntaxHighlighter.STYLES["keyword"]) for w in PySyntaxHighlighter.keywords]
+        rules += [(QtCore.QRegularExpression(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]
+        rules += [(QtCore.QRegularExpression(o), 0, PySyntaxHighlighter.STYLES["operator"]) for o in PySyntaxHighlighter.operators]
         # Braces
-        rules += [(QtCore.QRegExp(b), 0, PySyntaxHighlighter.STYLES["brace"]) for b in PySyntaxHighlighter.braces]
+        rules += [(QtCore.QRegularExpression(b), 0, PySyntaxHighlighter.STYLES["brace"]) for b in PySyntaxHighlighter.braces]
 
         # All other rules
         rules += [
             # self
-            (QtCore.QRegExp(r'\bself\b'), 0, PySyntaxHighlighter.STYLES["self"]),
+            (QtCore.QRegularExpression(r'\bself\b'), 0, PySyntaxHighlighter.STYLES["self"]),
 
             # 'def' followed by an identifier
-            (QtCore.QRegExp(r'\bdef\b\s*(\w+)'), 1, PySyntaxHighlighter.STYLES["deffunc"]),
+            (QtCore.QRegularExpression(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"]),
+            (QtCore.QRegularExpression(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"]),
+            (QtCore.QRegularExpression(r'\b[+-]?[0-9]+[lL]?\b'), 0, PySyntaxHighlighter.STYLES["numbers"]),
+            (QtCore.QRegularExpression(r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b'), 0, PySyntaxHighlighter.STYLES["numbers"]),
+            (QtCore.QRegularExpression(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"]),
+            (QtCore.QRegularExpression(r'"[^"\\]*(\\.[^"\\]*)*"'), 0, PySyntaxHighlighter.STYLES["string"]),
             # Single-quoted string, possibly containing escape sequences
-            (QtCore.QRegExp(r"'[^'\\]*(\\.[^'\\]*)*'"), 0, PySyntaxHighlighter.STYLES["string"]),
+            (QtCore.QRegularExpression(r"'[^'\\]*(\\.[^'\\]*)*'"), 0, PySyntaxHighlighter.STYLES["string"]),
 
             # From '#' until a newline
-            (QtCore.QRegExp(r'#[^\n]*'), 0, PySyntaxHighlighter.STYLES['comment']),
+            (QtCore.QRegularExpression(r'#[^\n]*'), 0, PySyntaxHighlighter.STYLES['comment']),
         ]
 
         return rules
@@ -250,14 +250,17 @@ def highlightBlock(self, text):
         # Do other syntax formatting
         for expression, nth, _format in self._rules:
             # fetch the index of the expression in text
-            index = expression.indexIn(text, 0)
+            match = expression.match(text, 0)
+            index = match.capturedStart()
 
             while index >= 0:
                 # We actually want the index of the nth match
-                index = expression.pos(nth)
-                length = len(expression.cap(nth))
+                index = match.capturedStart(nth)
+                length = len(match.captured(nth))
                 self.setFormat(index, length, _format)
-                index = expression.indexIn(text, index + length)
+                # index = expression.indexIn(text, index + length)
+                match = expression.match(text, index + length)
+                index = match.capturedStart()
 
     def textDoc(self):
         """ Returns the document being highlighted.
@@ -287,4 +290,4 @@ def setTextDocument(self, document):
     textDocumentChanged = Signal()
 
     # Property
-    textDocument = Property(QtQuick.QQuickTextDocument, textDoc, setTextDocument, notify=textDocumentChanged)
+    textDocument = Property(QObject, textDoc, setTextDocument, notify=textDocumentChanged)

From 8207e84a41a2faadca53b67eeb9034188bdf8b53 Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Tue, 26 Nov 2024 15:47:20 +0100
Subject: [PATCH 05/10] [ui] ScriptEditor: ScriptEditor gets a RowLayout

A Row Layout is more practical for using script editor with the current placement of meshroom GUI.
---
 meshroom/ui/qml/GraphEditor/ScriptEditor.qml | 73 ++++++++++----------
 1 file changed, 35 insertions(+), 38 deletions(-)

diff --git a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
index 1430613b56..7f474fd483 100644
--- a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
+++ b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
@@ -211,50 +211,16 @@ Item {
             }
         }
 
-        MSplitView {
-            id: topBottomSplit
+        RowLayout {
             Layout.fillHeight: true
             Layout.fillWidth: true
-
-            orientation: Qt.Vertical
-        
-            // Output Text Area -- Shows the output for the executed script(s)
-            Rectangle {
-                id: outputArea
-
-                // Has a minimum height
-                SplitView.minimumHeight: 80
-
-                color: palette.base
-
-                Flickable {
-                    width: parent.width
-                    height: parent.height
-                    contentWidth: width
-                    contentHeight: ( output.lineCount + 5 ) * output.font.pixelSize // + 5 lines for buffer to be scrolled and visibility
-
-                    ScrollBar.vertical: MScrollBar {}
-
-                    TextArea.flickable: TextArea {
-                        id: output
-
-                        readOnly: true
-                        selectByMouse: true
-                        padding: 0
-                        Layout.fillHeight: true
-                        Layout.fillWidth: true
-                        wrapMode: Text.WordWrap
-
-                        textFormat: Text.RichText
-                    }
-                }
-            }
+            width: root.width
 
             // Input Text Area -- Holds the input scripts to be executed
             Rectangle {
                 id: inputArea
-
-                SplitView.fillHeight: true
+                Layout.fillHeight: true
+                Layout.fillWidth: true
 
                 color: palette.base
 
@@ -342,6 +308,37 @@ Item {
                 }
             }
 
+            // Output Text Area -- Shows the output for the executed script(s)
+            Rectangle {
+                id: outputArea
+                Layout.fillHeight: true
+                Layout.fillWidth: true
+
+                color: palette.base
+
+                Flickable {
+                    width: parent.width
+                    height: parent.height
+                    contentWidth: width
+                    contentHeight: ( output.lineCount + 5 ) * output.font.pixelSize // + 5 lines for buffer to be scrolled and visibility
+
+                    ScrollBar.vertical: MScrollBar {}
+
+                    TextArea.flickable: TextArea {
+                        id: output
+
+                        readOnly: true
+                        selectByMouse: true
+                        padding: 0
+                        Layout.fillHeight: true
+                        Layout.fillWidth: true
+                        wrapMode: Text.WordWrap
+
+                        textFormat: Text.RichText
+                    }
+                }
+            }
+
             // Syntax Highlights for the Input Area for Python Based Syntax
             PySyntaxHighlighter {
                 id: syntaxHighlighter

From 49052dfc0ff3e3cbc34735a1ba757a69fd861d4f Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Thu, 12 Dec 2024 21:08:28 +0530
Subject: [PATCH 06/10] [ui] ScriptEditor: ScriptEditor gets new icons

Updated Icons for ScriptEditor

Script Editor shows a confirmation dialog before clearing history
---
 meshroom/ui/qml/Application.qml              |  1 +
 meshroom/ui/qml/GraphEditor/ScriptEditor.qml | 50 +++++++++++++++-----
 2 files changed, 40 insertions(+), 11 deletions(-)

diff --git a/meshroom/ui/qml/Application.qml b/meshroom/ui/qml/Application.qml
index 591399e6f9..8c4b696f98 100644
--- a/meshroom/ui/qml/Application.qml
+++ b/meshroom/ui/qml/Application.qml
@@ -1269,6 +1269,7 @@ Page {
                     ScriptEditor {
                         id: scriptEditor
                         anchors.fill: parent
+                        rootApplication: root
 
                         visible: graphEditorPanel.currentTab === 2
                     }
diff --git a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
index 7f474fd483..91612abe0e 100644
--- a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
+++ b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
@@ -14,8 +14,26 @@ import ScriptEditor 1.0
 Item {
     id: root
 
+    // Defines the parent or the root Application of which this script editor is a part of
+    property var rootApplication: undefined;
+
+    Component {
+        id: clearConfirmationDialog
+
+        MessageDialog {
+            title: "Clear history"
+
+            preset: "Warning"
+            text: "This will clear all history of executed scripts."
+            helperText: "Are you sure you would like to continue?."
+
+            standardButtons: Dialog.Ok | Dialog.Cancel
+            onClosed: destroy()
+        }
+    }
+
     function replace(text, string, replacement) {
-        /*
+        /**
          * Replaces all occurences of the string in the text
          * @param text - overall text
          * @param string - the string to be replaced in the text
@@ -28,7 +46,7 @@ Item {
     }
 
     function formatInput(text) {
-        /*
+        /**
          * Formats the text to be displayed as the input script executed
          */
 
@@ -37,7 +55,7 @@ Item {
     }
 
     function formatOutput(text) {
-        /*
+        /**
          * Formats the text to be displayed as the result of the script executed
          */
 
@@ -45,6 +63,15 @@ Item {
         return "<font color=#49a1f3>" + "Result: " + replace(text, "\n", "<br>") + "</font><br><br>"
     }
 
+    function clearHistory() {
+        /**
+         * Clears all of the executed history from the script editor
+         */
+        ScriptEditorManager.clearHistory()
+        input.clear()
+        output.clear()
+    }
+
     function processScript(text = "") {
         // Use either the provided/selected or the entire script
         text = text || input.text
@@ -117,7 +144,7 @@ Item {
 
             MaterialToolButton {
                 font.pointSize: 18
-                text: MaterialIcons.download
+                text: MaterialIcons.file_open
                 ToolTip.text: "Load Script"
 
                 onClicked: {
@@ -127,7 +154,7 @@ Item {
 
             MaterialToolButton {
                 font.pointSize: 18
-                text: MaterialIcons.upload
+                text: MaterialIcons.save
                 ToolTip.text: "Save Script"
 
                 onClicked: {
@@ -142,7 +169,7 @@ Item {
             MaterialToolButton {
                 id: executeButton
                 font.pointSize: 18
-                text: MaterialIcons.slideshow
+                text: MaterialIcons.play_arrow
                 ToolTip.text: "Execute Script"
 
                 onClicked: {
@@ -152,7 +179,7 @@ Item {
 
             MaterialToolButton {
                 font.pointSize: 18
-                text: MaterialIcons.cancel_presentation
+                text: MaterialIcons.backspace
                 ToolTip.text: "Clear Output Window"
 
                 onClicked: {
@@ -196,13 +223,14 @@ Item {
 
             MaterialToolButton {
                 font.pointSize: 18
-                text: MaterialIcons.backspace
+                text: MaterialIcons.delete_sweep
                 ToolTip.text: "Clear History"
 
                 onClicked: {
-                    ScriptEditorManager.clearHistory()
-                    input.clear()
-                    output.clear()
+                    // Confirm from the user before clearing out any history
+                    const confirmationDialog = clearConfirmationDialog.createObject(rootApplication ? rootApplication : root);
+                    confirmationDialog.accepted.connect(clearHistory);
+                    confirmationDialog.open();
                 }
             }
 

From a90d5c4d382e769ab777cf1841170207deff164b Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Wed, 8 Jan 2025 09:47:37 +0530
Subject: [PATCH 07/10] [ui] ScriptEditor: Updated the content width of the
 input and output flickables

Formatted the input and output text for output display text area
---
 meshroom/ui/qml/GraphEditor/ScriptEditor.qml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
index 91612abe0e..e00df208d0 100644
--- a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
+++ b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
@@ -51,7 +51,7 @@ Item {
          */
 
         // Replace the text to be RichText Supportive
-        return "<font color=#868686>" + replace(text, "\n", "<br>") + "</font><br><br>"
+        return "<font color=#868686>> Input:<br>" + replace(text, "\n", "<br>") + "</font><br>"
     }
 
     function formatOutput(text) {
@@ -60,7 +60,7 @@ Item {
          */
 
         // Replace the text to be RichText Supportive
-        return "<font color=#49a1f3>" + "Result: " + replace(text, "\n", "<br>") + "</font><br><br>"
+        return "<font color=#49a1f3>> Result:<br>" + replace(text, "\n", "<br>") + "</font><br>"
     }
 
     function clearHistory() {
@@ -295,7 +295,7 @@ Item {
                     width: parent.width
                     height: parent.height
                     contentWidth: width
-                    contentHeight: ( input.lineCount + 5 ) * input.font.pixelSize // + 5 lines for buffer to be scrolled and visibility
+                    contentHeight: input.contentHeight;
 
                     anchors.left: lineNumbers.right
                     anchors.top: parent.top
@@ -348,7 +348,7 @@ Item {
                     width: parent.width
                     height: parent.height
                     contentWidth: width
-                    contentHeight: ( output.lineCount + 5 ) * output.font.pixelSize // + 5 lines for buffer to be scrolled and visibility
+                    contentHeight: output.contentHeight;
 
                     ScrollBar.vertical: MScrollBar {}
 

From 9fa772442d46700966e37422196f7cb1fa547b45 Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Fri, 10 Jan 2025 07:43:04 +0100
Subject: [PATCH 08/10] [ui] ScriptEditorManager: Added properties to get if we
 have history of scripts

hasPreviousScript and hasNextScript are getters for history if that is available
---
 meshroom/ui/components/scriptEditor.py | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/meshroom/ui/components/scriptEditor.py b/meshroom/ui/components/scriptEditor.py
index 8ef82edcd0..7c60f31250 100644
--- a/meshroom/ui/components/scriptEditor.py
+++ b/meshroom/ui/components/scriptEditor.py
@@ -44,6 +44,18 @@ def _lastScript(self):
         settings = QSettings()
         settings.beginGroup(self._GROUP)
         return settings.value(self._KEY)
+    
+    def _hasPreviousScript(self):
+        """ Returns whether there is a previous script available.
+        """
+        # If the current index is greater than the first
+        return self._index > 0
+    
+    def _hasNextScript(self):
+        """ Returns whethere there is a new script available to load.
+        """
+        # If the current index is lower than the available indexes
+        return self._index < (len(self._history) - 1)
 
     # Public
     @Slot(str, result=str)
@@ -74,6 +86,7 @@ def process(self, script):
         # 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)
+        self.scriptIndexChanged.emit()
 
         return result
 
@@ -89,6 +102,7 @@ def getNextScript(self):
             If there is no next entry, return an empty string. """
         if self._index + 1 < len(self._history) and len(self._history) > 0:
             self._index = self._index + 1
+            self.scriptIndexChanged.emit()
             return self._history[self._index]
         return ""
 
@@ -98,6 +112,7 @@ def getPreviousScript(self):
             If there is no previous entry, return an empty string. """
         if self._index - 1 >= 0 and self._index - 1 < len(self._history):
             self._index = self._index - 1
+            self.scriptIndexChanged.emit()
             return self._history[self._index]
         elif self._index == 0 and len(self._history):
             return self._history[self._index]
@@ -120,6 +135,11 @@ def saveScript(self, script):
         settings.beginGroup(self._GROUP)
         settings.setValue(self._KEY, script)
         settings.sync()
+    
+    scriptIndexChanged = Signal()
+
+    hasPreviousScript = Property(bool, _hasPreviousScript, notify=scriptIndexChanged)
+    hasNextScript = Property(bool, _hasNextScript, notify=scriptIndexChanged)
 
 
 class CharFormat(QtGui.QTextCharFormat):

From 1b963ab1f2864b30a9850231c7164ab90622afc1 Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Fri, 10 Jan 2025 07:44:45 +0100
Subject: [PATCH 09/10] [ui] ScriptEditor: Adjusted Icon Size and Layout

---
 meshroom/ui/qml/GraphEditor/ScriptEditor.qml | 68 ++++++++++----------
 1 file changed, 34 insertions(+), 34 deletions(-)

diff --git a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
index e00df208d0..fc0f0c36e3 100644
--- a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
+++ b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
@@ -143,7 +143,7 @@ Item {
             Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
 
             MaterialToolButton {
-                font.pointSize: 18
+                font.pointSize: 13
                 text: MaterialIcons.file_open
                 ToolTip.text: "Load Script"
 
@@ -153,7 +153,7 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 18
+                font.pointSize: 13
                 text: MaterialIcons.save
                 ToolTip.text: "Save Script"
 
@@ -162,40 +162,13 @@ Item {
                 }
             }
 
-            Item {
-                width: executeButton.width
-            }
-
-            MaterialToolButton {
-                id: executeButton
-                font.pointSize: 18
-                text: MaterialIcons.play_arrow
-                ToolTip.text: "Execute Script"
-
-                onClicked: {
-                    root.processScript()
-                }
-            }
-
-            MaterialToolButton {
-                font.pointSize: 18
-                text: MaterialIcons.backspace
-                ToolTip.text: "Clear Output Window"
-
-                onClicked: {
-                    output.clear()
-                }
-            }
-
-            Item {
-                width: executeButton.width
-            }
-
             MaterialToolButton {
-                font.pointSize: 18
+                font.pointSize: 13
                 text: MaterialIcons.history
                 ToolTip.text: "Get Previous Script"
 
+                enabled: ScriptEditorManager.hasPreviousScript;
+
                 onClicked: {
                     var ret = ScriptEditorManager.getPreviousScript()
 
@@ -207,10 +180,12 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 18
+                font.pointSize: 13
                 text: MaterialIcons.update
                 ToolTip.text: "Get Next Script"
 
+                enabled: ScriptEditorManager.hasNextScript;
+
                 onClicked: {
                     var ret = ScriptEditorManager.getNextScript()
 
@@ -222,7 +197,7 @@ Item {
             }
 
             MaterialToolButton {
-                font.pointSize: 18
+                font.pointSize: 13
                 text: MaterialIcons.delete_sweep
                 ToolTip.text: "Clear History"
 
@@ -234,9 +209,34 @@ Item {
                 }
             }
 
+            Item {
+                width: executeButton.width;
+            }
+            
+            MaterialToolButton {
+                id: executeButton
+                font.pointSize: 13
+                text: MaterialIcons.play_arrow
+                ToolTip.text: "Execute Script"
+
+                onClicked: {
+                    root.processScript()
+                }
+            }
+
             Item {
                 Layout.fillWidth: true
             }
+
+            MaterialToolButton {
+                font.pointSize: 13
+                text: MaterialIcons.backspace
+                ToolTip.text: "Clear Output Window"
+
+                onClicked: {
+                    output.clear()
+                }
+            }
         }
 
         RowLayout {

From 7384db89e3d22bf18f5f650d7aee990dd1c20306 Mon Sep 17 00:00:00 2001
From: waaake <vivek_ve@outlook.com>
Date: Fri, 10 Jan 2025 08:18:43 +0100
Subject: [PATCH 10/10] [ui] ScriptEditor: Updated to Use Horizontal MSplitView

---
 meshroom/ui/qml/GraphEditor/ScriptEditor.qml | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
index fc0f0c36e3..dd784f9dd2 100644
--- a/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
+++ b/meshroom/ui/qml/GraphEditor/ScriptEditor.qml
@@ -239,16 +239,16 @@ Item {
             }
         }
 
-        RowLayout {
-            Layout.fillHeight: true
-            Layout.fillWidth: true
-            width: root.width
+        MSplitView {
+            id: scriptSplitView;
+            Layout.fillHeight: true;
+            Layout.fillWidth: true;
+            orientation: Qt.Horizontal;
 
             // Input Text Area -- Holds the input scripts to be executed
             Rectangle {
                 id: inputArea
-                Layout.fillHeight: true
-                Layout.fillWidth: true
+                SplitView.preferredWidth: root.width / 2;
 
                 color: palette.base