From 8984aac20e2b9dcb5ce1d99d965f6caca1f8caf2 Mon Sep 17 00:00:00 2001 From: waaake 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 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 "" + replace(text, "\n", "
") + "


" + } + + function formatOutput(text) { + /* + * Formats the text to be displayed as the result of the script executed + */ + + // Replace the text to be RichText Supportive + return "" + "Result: " + replace(text, "\n", "
") + "


" } - 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 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 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 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 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 "" + "Result: " + replace(text, "\n", "
") + "


" } + 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 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 "" + replace(text, "\n", "
") + "


" + return "> Input:
" + replace(text, "\n", "
") + "

" } function formatOutput(text) { @@ -60,7 +60,7 @@ Item { */ // Replace the text to be RichText Supportive - return "" + "Result: " + replace(text, "\n", "
") + "


" + return "> Result:
" + replace(text, "\n", "
") + "

" } 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 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 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 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