diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 64afaf85b1..570d36bfa3 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -26,7 +26,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.components.messaging import MessageController from meshroom.ui.palette import PaletteManager from meshroom.ui.reconstruction import Reconstruction from meshroom.ui.utils import QmlInstantEngine diff --git a/meshroom/ui/components/messaging.py b/meshroom/ui/components/messaging.py new file mode 100644 index 0000000000..a6cc722385 --- /dev/null +++ b/meshroom/ui/components/messaging.py @@ -0,0 +1,77 @@ +import json +from PySide6.QtCore import QObject +from datetime import datetime +from meshroom.common import Signal, Slot, Property + + +class Message: + def __init__(self, msg, status=None): + self.msg = msg + self.status = status or "info" + self.date = datetime.now() + + def dateStr(self, fullDate=False): + dateFormat = "%H:%M:%S" + if fullDate: + dateFormat = "%Y-%m-%d %H:%M:%S.%f" + return self.date.strftime(dateFormat) + + +class MessageController(QObject): + """ + Handles messages sent from the Python side to the StatusBar component + """ + + message = Signal(str, str, int) + messagesChanged = Signal() # Signal to notify when messages list changes + + def __init__(self, parent): + super().__init__(parent) + self._messages = [] + + def sendMessage(self, msg, status, duration): + """ Sends a message that will be displayed on the status bar """ + self.message.emit(msg, status, duration) + + @Slot(str, str) + def storeMessage(self, msg, status): + """ Adds a new message in the stack """ + self._messages.append(Message(msg, status or "info")) + self.messagesChanged.emit() # Notify QML that messages have changed + + def _getMessagesDict(self, fullDate=False): + """ Get a dict with all stored messages """ + messages = [] + for msg in self._messages: + messages.append({ + "status": msg.status, + "date": msg.dateStr(fullDate), + "text": msg.msg, + }) + return messages + + def getMessages(self): + """ Get the messages with simple date infos. + Reverse the list to make sure we see the most recent item on top + """ + return self._getMessagesDict()[::-1] + + @Slot(result=str) + def getMessagesAsString(self): + """ Return messages for clipboard copy + .. note:: + Could also do `json.dumps(self._getMessagesDict(fullDate=True), indent=4)` + """ + messages = [] + for msg in self._messages: + messages.append(f"{msg.dateStr(True)} [{msg.status.upper():<7}] {msg.msg}") + return "\n".join(messages) + + @Slot() + def clearMessages(self): + """ Clear all stored messages """ + self._messages.clear() + self.messagesChanged.emit() + + # Property to expose messages to QML + messages = Property("QVariantList", getMessages, notify=messagesChanged) diff --git a/meshroom/ui/components/statusBar.py b/meshroom/ui/components/statusBar.py deleted file mode 100644 index 1e51ddff32..0000000000 --- a/meshroom/ui/components/statusBar.py +++ /dev/null @@ -1,16 +0,0 @@ -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/Controls/StatusBar.qml b/meshroom/ui/qml/Controls/StatusBar.qml index 1340224b24..b9ce498d86 100644 --- a/meshroom/ui/qml/Controls/StatusBar.qml +++ b/meshroom/ui/qml/Controls/StatusBar.qml @@ -34,10 +34,11 @@ RowLayout { 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 + onClicked: { + var component = Qt.createComponent("StatusMessages.qml") + var window = component.createObject(root) + window.show() + } Component.onCompleted: { statusBarButton.contentItem.color = defaultColor } @@ -99,12 +100,14 @@ RowLayout { function showMessage(msg, status=undefined, duration=root.interval) { statusBar.showMessage(msg, status, duration) + // Add message to the message list + _messageController.storeMessage(msg, status) } Connections { target: _messageController function onMessage(message, color, duration) { - showMessage(message, color, duration) + root.showMessage(message, color, duration) } } } diff --git a/meshroom/ui/qml/Controls/StatusMessages.qml b/meshroom/ui/qml/Controls/StatusMessages.qml new file mode 100644 index 0000000000..69fd3358eb --- /dev/null +++ b/meshroom/ui/qml/Controls/StatusMessages.qml @@ -0,0 +1,168 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import MaterialIcons 2.2 +import Utils 1.0 + +ApplicationWindow { + id: root + title: "Messages" + width: 500 + height: 400 + minimumWidth: 350 + minimumHeight: 250 + + SystemPalette { id: systemPalette } + + function getColor(status) { + switch (status) { + case "ok": return Colors.green + case "warning": return Colors.orange + case "error": return Colors.red + default: return systemPalette.text + } + } + + function getBackgroundColor(status) { + var color = getColor(status) + var alphaValue = status == "info" ? 0.05 : 0.1 + return Qt.rgba(color.r, color.g, color.b, alphaValue) + } + + function getBorderColor(status) { + var color = getColor(status) + var alphaValue = status == "info" ? 0.2 : 0.3 + return Qt.rgba(color.r, color.g, color.b, alphaValue) + } + + function getStatusIcon(status) { + switch (status) { + case "ok": return MaterialIcons.check_circle + case "warning": return MaterialIcons.warning + case "error": return MaterialIcons.error + default: return MaterialIcons.info + } + } + + header: ToolBar { + + background: Rectangle { + implicitWidth: root.width + implicitHeight: 50 + color: Qt.darker(systemPalette.base, 1.2) + } + + RowLayout { + anchors.fill: parent + + Text { + Layout.fillWidth: true + text: "Messages (" + messageListView.count + ")" + font.bold: true + color: Qt.darker(systemPalette.text, 1.2) + } + + MaterialToolButton { + ToolTip.text: "Clear the message list" + text: MaterialIcons.clear_all + font.pointSize: 16 + palette.base: systemPalette.base + // Text color + Component.onCompleted: { + contentItem.color = Qt.darker(systemPalette.text, 1.2) + } + onClicked: _messageController.clearMessages() + } + + MaterialToolButton { + ToolTip.text: "Copy the messages" + text: MaterialIcons.content_copy + font.pointSize: 16 + palette.base: systemPalette.base + // Text color + Component.onCompleted: { + contentItem.color = Qt.darker(systemPalette.text, 1.2) + } + onClicked: { + var msgDict = _messageController.getMessagesAsString() + if (msgDict !== '') { + Clipboard.clear() + Clipboard.setText(msgDict) + } + } + } + } + } + + Rectangle { + anchors.fill: parent + color: systemPalette.base + + ScrollView { + anchors.fill: parent + anchors.margins: 10 + + ListView { + id: messageListView + model: _messageController.messages + verticalLayoutDirection: ListView.TopToBottom + spacing: 5 + + delegate: Rectangle { + width: messageListView.width + height: messageLayout.implicitHeight + 16 + color: root.getBackgroundColor(modelData.status) + border.color: root.getBorderColor(modelData.status) + border.width: 1 + radius: 4 + + RowLayout { + id: messageLayout + anchors.fill: parent + anchors.margins: 8 + spacing: 12 + + // Icon + Text { + text: root.getStatusIcon(modelData.status) + font.pointSize: 14 + color: root.getColor(modelData.status) + Layout.alignment: Qt.AlignVCenter + } + + // Text + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Text { + text: modelData.date + font.pointSize: 8 + color: Qt.darker(systemPalette.windowText, 1.5) + Layout.alignment: Qt.AlignLeft + } + + Text { + text: modelData.text + wrapMode: Text.WordWrap + Layout.fillWidth: true + color: systemPalette.windowText + font.pointSize: 10 + } + } + } + } + + // Empty state + Text { + anchors.centerIn: parent + text: "No message to display" + color: Qt.darker(systemPalette.windowText, 1.5) + font.pointSize: 12 + visible: messageListView.count === 0 + } + } + } + } +} \ No newline at end of file