From 161c2944ef49a18d96c3d6d884b26290906dbb3a Mon Sep 17 00:00:00 2001 From: Alice Sonolet Date: Tue, 16 Sep 2025 15:25:17 +0200 Subject: [PATCH 1/5] [ui] Implement status bar to display messages --- meshroom/core/node.py | 4 + meshroom/ui/app.py | 14 +++- meshroom/ui/qml/Application.qml | 16 +++- meshroom/ui/qml/Controls/StatusBar.qml | 101 +++++++++++++++++++++++++ meshroom/ui/qml/Controls/qmldir | 1 + meshroom/ui/reconstruction.py | 5 +- 6 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 meshroom/ui/qml/Controls/StatusBar.qml diff --git a/meshroom/core/node.py b/meshroom/core/node.py index c8d0185403..6c010e717d 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -522,6 +522,10 @@ def process(self, forceCompute=False, inCurrentEnv=False): executionStatus = None self.statThread = stats.StatisticsThread(self) self.statThread.start() + + # Display message + + try: self.node.nodeDesc.processChunk(self) # NOTE: this assumes saving the output attributes for each chunk diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 2f6c7e31ca..d4074957df 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -6,7 +6,7 @@ from PySide6 import __version__ as PySideVersion from PySide6 import QtCore -from PySide6.QtCore import QUrl, QJsonValue, qInstallMessageHandler, QtMsgType, QSettings +from PySide6.QtCore import QObject, QUrl, QJsonValue, qInstallMessageHandler, QtMsgType, QSettings from PySide6.QtGui import QIcon from PySide6.QtQml import QQmlDebuggingEnabler from PySide6.QtQuickControls2 import QQuickStyle @@ -353,6 +353,18 @@ def _pipelineTemplateNames(self): def reloadTemplateList(self): meshroom.core.initPipelines() self.pipelineTemplateFilesChanged.emit() + + @Slot() + def forceUIUpdate(self): + """ Force UI to process pending events + Necessary when we want to update the UI while a trigger is still running (e.g. reloadPlugins) + """ + self.processEvents() + + def showMessage(self, message, status=None, duration=5000): + root = self.engine.rootObjects()[0] + statusBar = root.findChild(QObject, "statusBar") + statusBar.showMessage(message, status, duration) def _retrieveThumbnailPath(self, filepath: str) -> str: """ diff --git a/meshroom/ui/qml/Application.qml b/meshroom/ui/qml/Application.qml index 0d60bc3d25..c2ebaec375 100644 --- a/meshroom/ui/qml/Application.qml +++ b/meshroom/ui/qml/Application.qml @@ -552,7 +552,9 @@ Page { text: "Reload Plugins Source Code" shortcut: "Ctrl+Shift+R" onTriggered: { + statusBar.showMessage("Reloading plugins...") _reconstruction.reloadPlugins() + statusBar.showMessage("Plugins reloaded !", "ok") } } @@ -1023,9 +1025,10 @@ Page { rightPadding: 4 palette.window: Qt.darker(activePalette.window, 1.15) - // Cache Folder RowLayout { + anchors.fill: parent spacing: 0 + MaterialToolButton { font.pointSize: 8 text: MaterialIcons.folder_open @@ -1040,6 +1043,17 @@ Page { color: Qt.darker(palette.text, 1.2) background: Item {} } + + // Spacer to push status bar to the right + Item { Layout.fillWidth: true } + + StatusBar { + id: statusBar + objectName: "statusBar" // Expose to python + height: parent.height + defaultColor: Qt.darker(palette.text, 1.2) + defaultIcon : MaterialIcons.comment + } } } diff --git a/meshroom/ui/qml/Controls/StatusBar.qml b/meshroom/ui/qml/Controls/StatusBar.qml new file mode 100644 index 0000000000..8b6a21d0d2 --- /dev/null +++ b/meshroom/ui/qml/Controls/StatusBar.qml @@ -0,0 +1,101 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import MaterialIcons 2.2 + +RowLayout { + id: root + + property color defaultColor: "white" + property string defaultIcon : MaterialIcons.circle + property int interval : 5000 + property bool logMessage : false + + TextField { + id: statusBarField + Layout.fillHeight: true + readOnly: true + selectByMouse: true + text: statusBar.message + color: defaultColor + background: Item {} + visible: statusBar.message !== "" + } + + // TODO : Idea for later : implement a ProgressBar here + + MaterialToolButton { + id: statusBarButton + Layout.fillHeight: true + Layout.preferredWidth: 17 + visible: true + font.pointSize: 8 + text: defaultIcon + ToolTip.text: "Open Messages UI" + // TODO : Open messages UI + // onClicked: statusBar.showMessage("NotImplementedError : Cannot open interface", "error", 2000) + Component.onCompleted: { + statusBarButton.contentItem.color = defaultColor + } + } + + Timer { + id: statusBarTimer + interval: root.interval + running: false + repeat: false + onTriggered: { + // Erase message and reset button icon + statusBar.message = "" + statusBarField.color = defaultColor + statusBarButton.contentItem.color = defaultColor + statusBarButton.text = defaultIcon + } + } + + QtObject { + id: statusBar + property string message: "" + + function showMessage(msg, status=undefined, duration=undefined) { + var textColor = defaultColor + var logMsg = "[Message][INFO]" + switch (status) { + case "ok": { + logMsg = "[Message][ERR]" + textColor = Qt.lighter("green", 1.6) + statusBarButton.text = MaterialIcons.check_circle + break + } + case "warning": { + logMsg = "[Message][WARN]" + textColor = Qt.lighter("yellow", 1.4) + statusBarButton.text = MaterialIcons.warning + break + } + case "error": { + logMsg = "[Message][ERR ]" + textColor = Qt.lighter("red", 1.4) + statusBarButton.text = MaterialIcons.error + break + } + default: { + statusBarButton.text = defaultIcon + } + } + if (logMessage === true) { + console.log(logMsg + " " + msg) + } + statusBarField.color = textColor + statusBarButton.contentItem.color = textColor + statusBar.message = msg + statusBarTimer.interval = duration !== undefined ? duration : root.interval + statusBarTimer.restart() + } + } + + function showMessage(msg, status=undefined, duration=undefined) { + statusBar.showMessage(msg, status, duration) + MeshroomApp.forceUIUpdate() + } +} diff --git a/meshroom/ui/qml/Controls/qmldir b/meshroom/ui/qml/Controls/qmldir index c11085ac7d..d3a3f3c359 100644 --- a/meshroom/ui/qml/Controls/qmldir +++ b/meshroom/ui/qml/Controls/qmldir @@ -21,3 +21,4 @@ SelectionBox 1.0 SelectionBox.qml SelectionLine 1.0 SelectionLine.qml DelegateSelectionBox 1.0 DelegateSelectionBox.qml DelegateSelectionLine 1.0 DelegateSelectionLine.qml +StatusBar 1.0 StatusBar.qml diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index e98ab70f12..24234107ba 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -7,8 +7,9 @@ from threading import Thread from typing import Callable -from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl, QSizeF, QPoint +from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl, QSizeF, QPoint, QRegularExpression from PySide6.QtGui import QMatrix4x4, QMatrix3x3, QQuaternion, QVector3D, QVector2D +from PySide6.QtQml import QQmlContext import meshroom.core import meshroom.common @@ -930,7 +931,7 @@ def setBuildingIntrinsics(self, value): displayedAttr2D = makeProperty(QObject, "_displayedAttr2D", displayedAttr2DChanged) displayedAttrs3DChanged = Signal() - displayedAttrs3D = Property(QObject, lambda self: self._displayedAttrs3D, notify=displayedAttrs3DChanged) + displayedAttrs3D = Property(QObject, lambda self: self._displayedAttrs3D, notify=displayedAttrs3DChanged) @Slot(QObject) def setActiveNode(self, node, categories=True, inputs=True): From 7ca4882a9aad69cac35e104138b18a5301f6d327 Mon Sep 17 00:00:00 2001 From: Alice Sonolet Date: Tue, 16 Sep 2025 15:25:18 +0200 Subject: [PATCH 2/5] [ui] reloadPlugins : add _onPluginsReloaded slot to avoid blocking the UI --- meshroom/core/node.py | 3 --- meshroom/ui/app.py | 8 +++++--- meshroom/ui/qml/Application.qml | 3 +-- meshroom/ui/qml/Controls/StatusBar.qml | 10 +++++----- meshroom/ui/reconstruction.py | 27 ++++++++++++++++++-------- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/meshroom/core/node.py b/meshroom/core/node.py index 6c010e717d..3df2185d91 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -523,9 +523,6 @@ def process(self, forceCompute=False, inCurrentEnv=False): self.statThread = stats.StatisticsThread(self) self.statThread.start() - # Display message - - try: self.node.nodeDesc.processChunk(self) # NOTE: this assumes saving the output attributes for each chunk diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index d4074957df..b54200c804 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -362,9 +362,11 @@ def forceUIUpdate(self): self.processEvents() def showMessage(self, message, status=None, duration=5000): - root = self.engine.rootObjects()[0] - statusBar = root.findChild(QObject, "statusBar") - statusBar.showMessage(message, status, duration) + root = self.engine.rootObjects() + if root: + statusBar = root[0].findChild(QObject, "statusBar") + if statusBar is not None: + statusBar.showMessage(message, status, duration) def _retrieveThumbnailPath(self, filepath: str) -> str: """ diff --git a/meshroom/ui/qml/Application.qml b/meshroom/ui/qml/Application.qml index c2ebaec375..6af40fff4d 100644 --- a/meshroom/ui/qml/Application.qml +++ b/meshroom/ui/qml/Application.qml @@ -553,8 +553,7 @@ Page { shortcut: "Ctrl+Shift+R" onTriggered: { statusBar.showMessage("Reloading plugins...") - _reconstruction.reloadPlugins() - statusBar.showMessage("Plugins reloaded !", "ok") + _reconstruction.reloadPlugins() // This will handle the message to show that it finished properly } } diff --git a/meshroom/ui/qml/Controls/StatusBar.qml b/meshroom/ui/qml/Controls/StatusBar.qml index 8b6a21d0d2..923f337094 100644 --- a/meshroom/ui/qml/Controls/StatusBar.qml +++ b/meshroom/ui/qml/Controls/StatusBar.qml @@ -59,22 +59,22 @@ RowLayout { function showMessage(msg, status=undefined, duration=undefined) { var textColor = defaultColor - var logMsg = "[Message][INFO]" + var logLevel = "info" switch (status) { case "ok": { - logMsg = "[Message][ERR]" + logLevel = "info" textColor = Qt.lighter("green", 1.6) statusBarButton.text = MaterialIcons.check_circle break } case "warning": { - logMsg = "[Message][WARN]" + logLevel = "warn" textColor = Qt.lighter("yellow", 1.4) statusBarButton.text = MaterialIcons.warning break } case "error": { - logMsg = "[Message][ERR ]" + logLevel = "error" textColor = Qt.lighter("red", 1.4) statusBarButton.text = MaterialIcons.error break @@ -84,7 +84,7 @@ RowLayout { } } if (logMessage === true) { - console.log(logMsg + " " + msg) + console.log("[Message][" + logLevel.toUpperCase().padEnd(5) + "] " + msg) } statusBarField.color = textColor statusBarButton.contentItem.color = textColor diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 24234107ba..9a6c60b5da 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -7,9 +7,8 @@ from threading import Thread from typing import Callable -from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl, QSizeF, QPoint, QRegularExpression +from PySide6.QtCore import QObject, Slot, Property, Signal, QUrl, QSizeF, QPoint from PySide6.QtGui import QMatrix4x4, QMatrix3x3, QQuaternion, QVector3D, QVector2D -from PySide6.QtQml import QQmlContext import meshroom.core import meshroom.common @@ -387,6 +386,9 @@ def __init__(self, undoStack: commands.UndoStack, taskManager: TaskManager, defa # react to internal graph changes to update those variables self.graphChanged.connect(self.onGraphChanged) + # Connect the pluginsReloaded signal to the onPluginsReloaded function + self.pluginsReloaded.connect(self._onPluginsReloaded) + self.setDefaultPipeline(defaultPipeline) def __del__(self): @@ -438,13 +440,20 @@ def reloadPlugins(self): The nodes in the graph will be updated to match the changes in the description, if there was any. """ - nodeTypes: list[str] = [] - for plugin in meshroom.core.pluginManager.getPlugins().values(): - for node in plugin.nodes.values(): - if node.reload(): - nodeTypes.append(node.nodeDescriptor.__name__) - + def _reloadPlugins(reconstruction): + nodeTypes: list[str] = [] + for plugin in meshroom.core.pluginManager.getPlugins().values(): + for node in plugin.nodes.values(): + if node.reload(): + nodeTypes.append(node.nodeDescriptor.__name__) + reconstruction.pluginsReloaded.emit(nodeTypes) + + self._workerThreads.apply_async(func=lambda: _reloadPlugins(self), args=()) + + @Slot(list) + def _onPluginsReloaded(self, nodeTypes: list): self._graph.reloadNodePlugins(nodeTypes) + self.parent().showMessage("Plugins reloaded !", "ok") @Slot() @Slot(str) @@ -932,6 +941,8 @@ def setBuildingIntrinsics(self, value): displayedAttrs3DChanged = Signal() displayedAttrs3D = Property(QObject, lambda self: self._displayedAttrs3D, notify=displayedAttrs3DChanged) + + pluginsReloaded = Signal(list) @Slot(QObject) def setActiveNode(self, node, categories=True, inputs=True): From 66355e1313a6cf5ed4c3405f8b1fc430ecded784 Mon Sep 17 00:00:00 2001 From: Alice Sonolet Date: Tue, 16 Sep 2025 15:25:18 +0200 Subject: [PATCH 3/5] [ui] statusBar: apply suggestions from the PR review --- meshroom/ui/qml/Controls/StatusBar.qml | 2 +- meshroom/ui/reconstruction.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/meshroom/ui/qml/Controls/StatusBar.qml b/meshroom/ui/qml/Controls/StatusBar.qml index 923f337094..9c8d450ff8 100644 --- a/meshroom/ui/qml/Controls/StatusBar.qml +++ b/meshroom/ui/qml/Controls/StatusBar.qml @@ -91,11 +91,11 @@ RowLayout { statusBar.message = msg statusBarTimer.interval = duration !== undefined ? duration : root.interval statusBarTimer.restart() + MeshroomApp.forceUIUpdate() } } function showMessage(msg, status=undefined, duration=undefined) { statusBar.showMessage(msg, status, duration) - MeshroomApp.forceUIUpdate() } } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index 9a6c60b5da..d29dc2301a 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -435,20 +435,21 @@ def onCameraInitChanged(self): @Slot() def reloadPlugins(self): + """ Launch _reloadPlugins in a worker thread to avoid blocking the ui. """ + self._workerThreads.apply_async(func=self._reloadPlugins, args=()) + + def _reloadPlugins(self): """ Reload all the NodePlugins from all the registered plugins. The nodes in the graph will be updated to match the changes in the description, if there was any. """ - def _reloadPlugins(reconstruction): - nodeTypes: list[str] = [] - for plugin in meshroom.core.pluginManager.getPlugins().values(): - for node in plugin.nodes.values(): - if node.reload(): - nodeTypes.append(node.nodeDescriptor.__name__) - reconstruction.pluginsReloaded.emit(nodeTypes) - - self._workerThreads.apply_async(func=lambda: _reloadPlugins(self), args=()) + nodeTypes: list[str] = [] + for plugin in meshroom.core.pluginManager.getPlugins().values(): + for node in plugin.nodes.values(): + if node.reload(): + nodeTypes.append(node.nodeDescriptor.__name__) + self.pluginsReloaded.emit(nodeTypes) @Slot(list) def _onPluginsReloaded(self, nodeTypes: list): From 4522743a5c775f5b872a5cf8f2e85763c644af04 Mon Sep 17 00:00:00 2001 From: Alice Sonolet Date: Tue, 16 Sep 2025 15:25:19 +0200 Subject: [PATCH 4/5] [ui] StatusBar: Use context property to send messages to the status bar + minor adjustments on the UI --- meshroom/ui/app.py | 9 ++++----- meshroom/ui/components/statusBar.py | 16 +++++++++++++++ meshroom/ui/qml/Application.qml | 1 - meshroom/ui/qml/Controls/StatusBar.qml | 27 ++++++++++++++++---------- meshroom/ui/reconstruction.py | 2 +- 5 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 meshroom/ui/components/statusBar.py diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index b54200c804..325c5df406 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -25,6 +25,7 @@ from meshroom.ui.components.scene3D import Scene3DHelper, Transformations3DHelper from meshroom.ui.components.scriptEditor import ScriptEditorManager from meshroom.ui.components.thumbnail import ThumbnailCache +from meshroom.ui.components.statusBar import MessageController from meshroom.ui.palette import PaletteManager from meshroom.ui.reconstruction import Reconstruction from meshroom.ui.utils import QmlInstantEngine @@ -281,6 +282,8 @@ def __init__(self, inputArgs): self.engine.rootContext().setContextProperty("ThumbnailCache", ThumbnailCache(parent=self)) # additional context properties + self._messageController = MessageController(parent=self) + self.engine.rootContext().setContextProperty("_messageController", self._messageController) self.engine.rootContext().setContextProperty("_PaletteManager", PaletteManager(self.engine, parent=self)) self.engine.rootContext().setContextProperty("ScriptEditorManager", ScriptEditorManager(parent=self)) self.engine.rootContext().setContextProperty("MeshroomApp", self) @@ -362,11 +365,7 @@ def forceUIUpdate(self): self.processEvents() def showMessage(self, message, status=None, duration=5000): - root = self.engine.rootObjects() - if root: - statusBar = root[0].findChild(QObject, "statusBar") - if statusBar is not None: - statusBar.showMessage(message, status, duration) + self._messageController.sendMessage(message, status, duration) def _retrieveThumbnailPath(self, filepath: str) -> str: """ diff --git a/meshroom/ui/components/statusBar.py b/meshroom/ui/components/statusBar.py new file mode 100644 index 0000000000..1e51ddff32 --- /dev/null +++ b/meshroom/ui/components/statusBar.py @@ -0,0 +1,16 @@ +from PySide6.QtCore import QObject, Signal + + +class MessageController(QObject): + """ + Handles messages sent from the Python side to the StatusBar component + """ + + message = Signal(str, str, int) + + def __init__(self, parent): + super().__init__(parent) + + def sendMessage(self, msg, status, duration): + """ Sends a message that will be displayed on the status bar """ + self.message.emit(msg, status, duration) diff --git a/meshroom/ui/qml/Application.qml b/meshroom/ui/qml/Application.qml index 6af40fff4d..1e71fb1bb1 100644 --- a/meshroom/ui/qml/Application.qml +++ b/meshroom/ui/qml/Application.qml @@ -1050,7 +1050,6 @@ Page { id: statusBar objectName: "statusBar" // Expose to python height: parent.height - defaultColor: Qt.darker(palette.text, 1.2) defaultIcon : MaterialIcons.comment } } diff --git a/meshroom/ui/qml/Controls/StatusBar.qml b/meshroom/ui/qml/Controls/StatusBar.qml index 9c8d450ff8..91b3f8077f 100644 --- a/meshroom/ui/qml/Controls/StatusBar.qml +++ b/meshroom/ui/qml/Controls/StatusBar.qml @@ -6,7 +6,7 @@ import MaterialIcons 2.2 RowLayout { id: root - property color defaultColor: "white" + property color defaultColor: Qt.darker(palette.text, 1.2) property string defaultIcon : MaterialIcons.circle property int interval : 5000 property bool logMessage : false @@ -34,6 +34,8 @@ RowLayout { ToolTip.text: "Open Messages UI" // TODO : Open messages UI // onClicked: statusBar.showMessage("NotImplementedError : Cannot open interface", "error", 2000) + enabled: false // TODO: to remove when implemented + ToolTip.visible: false // TODO: to remove when implemented Component.onCompleted: { statusBarButton.contentItem.color = defaultColor } @@ -57,25 +59,24 @@ RowLayout { id: statusBar property string message: "" - function showMessage(msg, status=undefined, duration=undefined) { + function showMessage(msg, status=undefined, duration=root.interval) { var textColor = defaultColor var logLevel = "info" switch (status) { case "ok": { - logLevel = "info" - textColor = Qt.lighter("green", 1.6) + statusBarField.color = Colors.green statusBarButton.text = MaterialIcons.check_circle break } case "warning": { logLevel = "warn" - textColor = Qt.lighter("yellow", 1.4) + statusBarField.color = Colors.orange statusBarButton.text = MaterialIcons.warning break } case "error": { logLevel = "error" - textColor = Qt.lighter("red", 1.4) + statusBarField.color = Colors.red statusBarButton.text = MaterialIcons.error break } @@ -86,16 +87,22 @@ RowLayout { if (logMessage === true) { console.log("[Message][" + logLevel.toUpperCase().padEnd(5) + "] " + msg) } - statusBarField.color = textColor - statusBarButton.contentItem.color = textColor + statusBarButton.contentItem.color = statusBarField.color statusBar.message = msg - statusBarTimer.interval = duration !== undefined ? duration : root.interval + statusBarTimer.interval = duration statusBarTimer.restart() MeshroomApp.forceUIUpdate() } } - function showMessage(msg, status=undefined, duration=undefined) { + function showMessage(msg, status=undefined, duration=root.interval) { statusBar.showMessage(msg, status, duration) } + + Connections { + target: _messageController + function onMessage(message, color, duration) { + showMessage(message, color, duration) + } + } } diff --git a/meshroom/ui/reconstruction.py b/meshroom/ui/reconstruction.py index d29dc2301a..103527c2fe 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -454,7 +454,7 @@ def _reloadPlugins(self): @Slot(list) def _onPluginsReloaded(self, nodeTypes: list): self._graph.reloadNodePlugins(nodeTypes) - self.parent().showMessage("Plugins reloaded !", "ok") + self.parent().showMessage("Plugins reloaded!", "ok") @Slot() @Slot(str) From fe4fe0c3f94a8e2428ede816a7a1d5686dc13ec2 Mon Sep 17 00:00:00 2001 From: Alice Sonolet Date: Tue, 16 Sep 2025 15:25:20 +0200 Subject: [PATCH 5/5] [qml] Add missing import --- meshroom/ui/qml/Controls/StatusBar.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/meshroom/ui/qml/Controls/StatusBar.qml b/meshroom/ui/qml/Controls/StatusBar.qml index 91b3f8077f..1340224b24 100644 --- a/meshroom/ui/qml/Controls/StatusBar.qml +++ b/meshroom/ui/qml/Controls/StatusBar.qml @@ -3,6 +3,8 @@ import QtQuick.Controls import QtQuick.Layouts import MaterialIcons 2.2 +import Utils 1.0 + RowLayout { id: root