diff --git a/meshroom/core/node.py b/meshroom/core/node.py index c8d0185403..3df2185d91 100644 --- a/meshroom/core/node.py +++ b/meshroom/core/node.py @@ -522,6 +522,7 @@ def process(self, forceCompute=False, inCurrentEnv=False): executionStatus = None self.statThread = stats.StatisticsThread(self) self.statThread.start() + 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..325c5df406 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 @@ -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) @@ -353,6 +356,16 @@ 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): + 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 0d60bc3d25..1e71fb1bb1 100644 --- a/meshroom/ui/qml/Application.qml +++ b/meshroom/ui/qml/Application.qml @@ -552,7 +552,8 @@ Page { text: "Reload Plugins Source Code" shortcut: "Ctrl+Shift+R" onTriggered: { - _reconstruction.reloadPlugins() + statusBar.showMessage("Reloading plugins...") + _reconstruction.reloadPlugins() // This will handle the message to show that it finished properly } } @@ -1023,9 +1024,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 +1042,16 @@ 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 + 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..1340224b24 --- /dev/null +++ b/meshroom/ui/qml/Controls/StatusBar.qml @@ -0,0 +1,110 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import MaterialIcons 2.2 + +import Utils 1.0 + +RowLayout { + id: root + + property color defaultColor: Qt.darker(palette.text, 1.2) + 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) + enabled: false // TODO: to remove when implemented + ToolTip.visible: false // TODO: to remove when implemented + 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=root.interval) { + var textColor = defaultColor + var logLevel = "info" + switch (status) { + case "ok": { + statusBarField.color = Colors.green + statusBarButton.text = MaterialIcons.check_circle + break + } + case "warning": { + logLevel = "warn" + statusBarField.color = Colors.orange + statusBarButton.text = MaterialIcons.warning + break + } + case "error": { + logLevel = "error" + statusBarField.color = Colors.red + statusBarButton.text = MaterialIcons.error + break + } + default: { + statusBarButton.text = defaultIcon + } + } + if (logMessage === true) { + console.log("[Message][" + logLevel.toUpperCase().padEnd(5) + "] " + msg) + } + statusBarButton.contentItem.color = statusBarField.color + statusBar.message = msg + statusBarTimer.interval = duration + statusBarTimer.restart() + MeshroomApp.forceUIUpdate() + } + } + + 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/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..103527c2fe 100755 --- a/meshroom/ui/reconstruction.py +++ b/meshroom/ui/reconstruction.py @@ -386,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): @@ -432,6 +435,10 @@ 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 @@ -442,8 +449,12 @@ def reloadPlugins(self): 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): self._graph.reloadNodePlugins(nodeTypes) + self.parent().showMessage("Plugins reloaded!", "ok") @Slot() @Slot(str) @@ -930,7 +941,9 @@ 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) + + pluginsReloaded = Signal(list) @Slot(QObject) def setActiveNode(self, node, categories=True, inputs=True):