From 6a912a99953d92f7a3b4085a6a252956d4002880 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Tue, 24 Nov 2020 23:31:18 +0100 Subject: [PATCH] Add autosave capabilites to script editor --- libnymea-app/libnymea-app-core.h | 2 + libnymea-app/libnymea-app.pro | 2 + libnymea-app/scripting/codecompletion.cpp | 4 +- libnymea-app/scripting/scriptautosaver.cpp | 114 +++++++++++++++++++++ libnymea-app/scripting/scriptautosaver.h | 58 +++++++++++ nymea-app/resources.qrc | 1 + nymea-app/ui/MainPage.qml | 103 +++++++++++-------- nymea-app/ui/Nymea.qml | 5 + nymea-app/ui/components/InfoPane.qml | 93 +++++++++++++++++ nymea-app/ui/components/MainPageTile.qml | 8 +- nymea-app/ui/magic/ScriptEditor.qml | 69 +++++++++---- 11 files changed, 388 insertions(+), 71 deletions(-) create mode 100644 libnymea-app/scripting/scriptautosaver.cpp create mode 100644 libnymea-app/scripting/scriptautosaver.h create mode 100644 nymea-app/ui/components/InfoPane.qml diff --git a/libnymea-app/libnymea-app-core.h b/libnymea-app/libnymea-app-core.h index 4345c008..987b6331 100644 --- a/libnymea-app/libnymea-app-core.h +++ b/libnymea-app/libnymea-app-core.h @@ -107,6 +107,7 @@ #include "scriptmanager.h" #include "scripting/codecompletion.h" #include "scripting/completionmodel.h" +#include "scripting/scriptautosaver.h" #include "types/script.h" #include "types/scripts.h" #include "types/types.h" @@ -309,6 +310,7 @@ void registerQmlTypes() { qmlRegisterType(uri, 1, 0, "ScriptSyntaxHighlighter"); qmlRegisterType(uri, 1, 0, "CodeCompletion"); qmlRegisterUncreatableType(uri, 1, 0, "CompletionModel", "Get it from ScriptSyntaxHighlighter"); + qmlRegisterType(uri, 1, 0, "ScriptAutoSaver"); qmlRegisterType(uri, 1, 0, "UserManager"); qmlRegisterUncreatableType(uri, 1, 0, "UserInfo", "Get it from UserManager"); diff --git a/libnymea-app/libnymea-app.pro b/libnymea-app/libnymea-app.pro index 3f7bfff4..273e7d6b 100644 --- a/libnymea-app/libnymea-app.pro +++ b/libnymea-app/libnymea-app.pro @@ -33,6 +33,7 @@ SOURCES += \ ruletemplates/calendaritemtemplate.cpp \ ruletemplates/timedescriptortemplate.cpp \ ruletemplates/timeeventitemtemplate.cpp \ + scripting/scriptautosaver.cpp \ types/browseritem.cpp \ types/browseritems.cpp \ types/networkdevice.cpp \ @@ -170,6 +171,7 @@ HEADERS += \ ruletemplates/calendaritemtemplate.h \ ruletemplates/timedescriptortemplate.h \ ruletemplates/timeeventitemtemplate.h \ + scripting/scriptautosaver.h \ types/browseritem.h \ types/browseritems.h \ types/networkdevice.h \ diff --git a/libnymea-app/scripting/codecompletion.cpp b/libnymea-app/scripting/codecompletion.cpp index 995d0145..0c04f37c 100644 --- a/libnymea-app/scripting/codecompletion.cpp +++ b/libnymea-app/scripting/codecompletion.cpp @@ -530,7 +530,7 @@ void CodeCompletion::update() BlockInfo blockInfo = getBlockInfo(m_cursor.position()); // If we're inside a class, add properties - qDebug() << "Block name" << blockInfo.name; +// qDebug() << "Block name" << blockInfo.name; if (!blockInfo.name.isEmpty()) { foreach (const QString &s, m_classes.value(blockInfo.name).properties) { @@ -561,7 +561,7 @@ void CodeCompletion::update() m_model->update(entries); blockText.remove(QRegExp(".* ")); m_proxy->setFilter(blockText); - qDebug() << "Model has" << m_model->rowCount() << "Filtered:" << m_proxy->rowCount() << "filter:" << blockText; +// qDebug() << "Model has" << m_model->rowCount() << "Filtered:" << m_proxy->rowCount() << "filter:" << blockText; return; } diff --git a/libnymea-app/scripting/scriptautosaver.cpp b/libnymea-app/scripting/scriptautosaver.cpp new file mode 100644 index 00000000..449351a2 --- /dev/null +++ b/libnymea-app/scripting/scriptautosaver.cpp @@ -0,0 +1,114 @@ +#include "scriptautosaver.h" + +#include +#include +#include + +ScriptAutoSaver::ScriptAutoSaver(QObject *parent) : QObject(parent) +{ + +} + +ScriptAutoSaver::~ScriptAutoSaver() +{ + storeContent(); +} + +void ScriptAutoSaver::classBegin() +{ + +} + +void ScriptAutoSaver::componentComplete() +{ + +} + +bool ScriptAutoSaver::available() const +{ + return m_cacheFile.isOpen(); +} + +bool ScriptAutoSaver::active() const +{ + return m_active; +} + +void ScriptAutoSaver::setActive(bool active) +{ + if (m_active != active) { + m_active = active; + emit activeChanged(); + + storeContent(); + } +} + +QUuid ScriptAutoSaver::scriptId() const +{ + return m_scriptId; +} + +void ScriptAutoSaver::setScriptId(const QUuid &scriptId) +{ + if (m_scriptId != scriptId) { + m_scriptId = scriptId; + emit scriptIdChanged(); + + if (m_cacheFile.isOpen()) { + m_cacheFile.close(); + emit availableChanged(); + } + + QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/scripts/"; + QDir dir(path); + if (!dir.exists() && !dir.mkpath(path)) { + qWarning() << "Cannot create cache directory. Autosaving will not work..."; + return; + } + QString fileName = path + m_scriptId.toString().remove(QRegExp("[{}]")) + ".qml.autosave"; + m_cacheFile.setFileName(fileName); + if (!m_cacheFile.open(QFile::ReadWrite)) { + qWarning() << "Cannot open cache file. Autosaving will not work..."; + return; + } + m_cachedContent = QString::fromUtf8(m_cacheFile.readAll()); + emit cachedContentChanged(); + emit availableChanged(); + } +} + +QString ScriptAutoSaver::liveContent() const +{ + return m_liveContent; +} + +void ScriptAutoSaver::setLiveContent(const QString &liveContent) +{ + if (m_liveContent != liveContent) { + m_liveContent = liveContent; + emit liveContentChanged(); + + storeContent(); + } +} + +QString ScriptAutoSaver::cachedContent() const +{ + return m_cachedContent; +} + +void ScriptAutoSaver::storeContent() +{ + if (m_cacheFile.isOpen() && m_active && m_liveContent != m_cachedContent) { + qDebug() << "autosaving..."; + m_cacheFile.seek(0); + m_cacheFile.resize(0); + m_cacheFile.write(m_liveContent.toUtf8()); + m_cacheFile.flush(); + + m_cachedContent = m_liveContent; + emit cachedContentChanged(); + } + +} diff --git a/libnymea-app/scripting/scriptautosaver.h b/libnymea-app/scripting/scriptautosaver.h new file mode 100644 index 00000000..bc4d482c --- /dev/null +++ b/libnymea-app/scripting/scriptautosaver.h @@ -0,0 +1,58 @@ +#ifndef SCRIPTAUTOSAVER_H +#define SCRIPTAUTOSAVER_H + +#include +#include +#include +#include + +class ScriptAutoSaver : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_PROPERTY(QUuid scriptId READ scriptId WRITE setScriptId NOTIFY scriptIdChanged) + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(QString liveContent READ liveContent WRITE setLiveContent NOTIFY liveContentChanged) + Q_PROPERTY(QString cachedContent READ cachedContent NOTIFY cachedContentChanged) + +public: + explicit ScriptAutoSaver(QObject *parent = nullptr); + ~ScriptAutoSaver(); + + void classBegin() override; + void componentComplete() override; + + bool available() const; + + bool active() const; + void setActive(bool active); + + QUuid scriptId() const; + void setScriptId(const QUuid &scriptId); + + QString liveContent() const; + void setLiveContent(const QString &liveContent); + + QString cachedContent() const; + +signals: + void scriptIdChanged(); + void availableChanged(); + void activeChanged(); + void liveContentChanged(); + void cachedContentChanged(); + +private slots: + void storeContent(); + +private: + QUuid m_scriptId; + QString m_cachedContent; + QString m_liveContent; + + QFile m_cacheFile; + + bool m_active = false; +}; + +#endif // SCRIPTAUTOSAVER_H diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index e9f818f9..e91ed782 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -222,5 +222,6 @@ ui/components/UpdateStatusIcon.qml ui/magic/WriteNfcTagPage.qml ui/components/MediaPlayer.qml + ui/components/InfoPane.qml diff --git a/nymea-app/ui/MainPage.qml b/nymea-app/ui/MainPage.qml index 45af270c..6289f612 100644 --- a/nymea-app/ui/MainPage.qml +++ b/nymea-app/ui/MainPage.qml @@ -196,55 +196,70 @@ Page { anchors.fill: parent spacing: 0 - Pane { + InfoPane { Layout.fillWidth: true - Layout.preferredHeight: shownHeight - property int shownHeight: shown ? contentRow.implicitHeight : 0 - property bool shown: updatesModel.count > 0 || engine.systemController.updateRunning - visible: shownHeight > 0 - Behavior on shownHeight { NumberAnimation { easing.type: Easing.InOutQuad; duration: 150 } } - Material.elevation: 2 - padding: 0 + shown: updatesModel.count > 0 || engine.systemController.updateRunning + text: engine.systemController.updateRunning ? qsTr("System update in progress...") : qsTr("%n system update(s) available", "", updatesModel.count) + imageSource: "../images/system-update.svg" + rotatingIcon: engine.systemController.updateRunning + onPaneClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) - MouseArea { - anchors.fill: parent - onClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) - } - - Rectangle { - color: app.accentColor - anchors.fill: parent - - PackagesFilterModel { - id: updatesModel - packages: engine.systemController.packages - updatesOnly: true - } - - RowLayout { - id: contentRow - anchors { left: parent.left; top: parent.top; right: parent.right; leftMargin: app.margins; rightMargin: app.margins } - Item { - Layout.fillWidth: true - height: app.iconSize - } - - Label { - text: engine.systemController.updateRunning ? qsTr("System update in progress...") : qsTr("%n system update(s) available", "", updatesModel.count) - color: "white" - font.pixelSize: app.smallFont - } - ColorIcon { - height: app.iconSize / 2 - width: height - color: "white" - name: "../images/system-update.svg" - RotationAnimation on rotation { from: 0; to: 360; duration: 2000; loops: Animation.Infinite; running: engine.systemController.updateRunning } - } - } + PackagesFilterModel { + id: updatesModel + packages: engine.systemController.packages + updatesOnly: true } } +// Pane { +// Layout.fillWidth: true +// Layout.preferredHeight: shownHeight +// property int shownHeight: shown ? contentRow.implicitHeight : 0 +// property bool shown: updatesModel.count > 0 || engine.systemController.updateRunning +// visible: shownHeight > 0 +// Behavior on shownHeight { NumberAnimation { easing.type: Easing.InOutQuad; duration: 150 } } +// Material.elevation: 2 +// padding: 0 + +// MouseArea { +// anchors.fill: parent +// onClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) +// } + +// Rectangle { +// color: app.accentColor +// anchors.fill: parent + +// PackagesFilterModel { +// id: updatesModel +// packages: engine.systemController.packages +// updatesOnly: true +// } + +// RowLayout { +// id: contentRow +// anchors { left: parent.left; top: parent.top; right: parent.right; leftMargin: app.margins; rightMargin: app.margins } +// Item { +// Layout.fillWidth: true +// height: app.iconSize +// } + +// Label { +// text: engine.systemController.updateRunning ? qsTr("System update in progress...") : qsTr("%n system update(s) available", "", updatesModel.count) +// color: "white" +// font.pixelSize: app.smallFont +// } +// ColorIcon { +// height: app.iconSize / 2 +// width: height +// color: "white" +// name: "../images/system-update.svg" +// RotationAnimation on rotation { from: 0; to: 360; duration: 2000; loops: Animation.Infinite; running: engine.systemController.updateRunning } +// } +// } +// } +// } + Item { id: contentContainer Layout.fillWidth: true diff --git a/nymea-app/ui/Nymea.qml b/nymea-app/ui/Nymea.qml index 30495b61..509b1fab 100644 --- a/nymea-app/ui/Nymea.qml +++ b/nymea-app/ui/Nymea.qml @@ -59,7 +59,12 @@ ApplicationWindow { property int smallFont: 13 property int mediumFont: 16 property int largeFont: 20 + + property int smallIconSize: 16 property int iconSize: 24 + property int largeIconSize: 32 + property int hugeIconSize: 40 + property int delegateHeight: 60 property color backgroundColor: Material.background diff --git a/nymea-app/ui/components/InfoPane.qml b/nymea-app/ui/components/InfoPane.qml new file mode 100644 index 00000000..1acea6a0 --- /dev/null +++ b/nymea-app/ui/components/InfoPane.qml @@ -0,0 +1,93 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.2 + +Item { + id: root + implicitHeight: d.shownHeight + visible: d.shownHeight > 0 + + property alias text: textLabel.text + property alias imageSource: icon.name + property alias buttonText: button.text + + property alias color: background.color + property alias textColor: textLabel.color + + property bool rotatingIcon: false + + property bool shown: false + + function show() { + shown = true; + } + function hide() { + shown = false; + } + + signal paneClicked(); + signal buttonClicked(); + + QtObject { + id: d + property int shownHeight: shown ? contentRow.implicitHeight : 0 + Behavior on shownHeight { NumberAnimation { easing.type: Easing.InOutQuad; duration: 150 } } + } + + Pane { + anchors { left: parent.left; bottom: parent.bottom; right: parent.right } + Material.elevation: 2 + padding: 0 + height: contentRow.implicitHeight + + MouseArea { + anchors.fill: parent + onClicked: root.paneClicked() + } + + Rectangle { + id: background + color: app.accentColor + anchors.fill: parent + + RowLayout { + id: contentRow + anchors { left: parent.left; top: parent.top; right: parent.right; leftMargin: app.margins; rightMargin: app.margins } + Label { + id: textLabel + color: "white" + font.pixelSize: app.smallFont + Layout.fillWidth: true + Layout.margins: app.margins * .4 + horizontalAlignment: Text.AlignRight + wrapMode: Text.WordWrap + } + ColorIcon { + id: icon + height: app.smallIconSize + width: height + color: "white" + visible: name.length > 0 + + RotationAnimation on rotation { + from: 0 + to: 360 + duration: 2000 + loops: Animation.Infinite + running: root.rotatingIcon + onStopped: icon.rotation = 0; + } + } + + Button { + id: button + visible: text.length > 0 + onClicked: root.buttonClicked() + } + } + } + } +} + + diff --git a/nymea-app/ui/components/MainPageTile.qml b/nymea-app/ui/components/MainPageTile.qml index 4565ed46..bd46ce66 100644 --- a/nymea-app/ui/components/MainPageTile.qml +++ b/nymea-app/ui/components/MainPageTile.qml @@ -92,7 +92,7 @@ Item { ColorIcon { id: colorIcon anchors.centerIn: parent - height: app.iconSize * 1.5 + height: app.hugeIconSize //* 1.5 width: height ColorIcon { id: fallbackIcon @@ -149,21 +149,21 @@ Item { anchors { top: parent.top; right: parent.right; margins: app.margins } spacing: app.margins / 2 ColorIcon { - height: app.iconSize / 2 + height: app.smallIconSize width: height name: root.isWireless ? "../images/connections/nm-signal-00.svg" : "../images/connections/network-wired-offline.svg" color: root.disconnected ? "red" : "orange" visible: root.setupStatus == Thing.ThingSetupStatusComplete && (root.disconnected || (root.isWireless && root.signalStrength < 20)) } ColorIcon { - height: app.iconSize / 2 + height: app.smallIconSize width: height name: root.setupStatus === Thing.ThingSetupStatusFailed ? "../images/dialog-warning-symbolic.svg" : "../images/settings.svg" color: root.setupStatus === Thing.ThingSetupStatusFailed ? "red" : keyColor visible: root.setupStatus === Thing.ThingSetupStatusFailed || root.setupStatus === Thing.ThingSetupStatusInProgress } ColorIcon { - height: app.iconSize / 2 + height: app.smallIconSize width: height name: "../images/battery/battery-010.svg" visible: root.setupStatus == Thing.ThingSetupStatusComplete && root.batteryCritical diff --git a/nymea-app/ui/magic/ScriptEditor.qml b/nymea-app/ui/magic/ScriptEditor.qml index cf9b0129..ae383235 100644 --- a/nymea-app/ui/magic/ScriptEditor.qml +++ b/nymea-app/ui/magic/ScriptEditor.qml @@ -139,38 +139,43 @@ Page { font: scriptEdit.font } + ScriptAutoSaver { + id: autoSaver + scriptId: d.scriptId + liveContent: scriptEdit.text + } + Connections { target: engine.scriptManager - onAddScriptReply: { - if (id == d.callId) { + onAddScriptReply: deployReply(id, scriptError, errors) + onEditScriptReply: deployReply(id, scriptError, errors) + function deployReply(id, scriptError, errors) { + if (id === d.callId) { d.callId = -1; - if (scriptError == "ScriptErrorNoError") { - d.scriptId = scriptId; + if (scriptError === "ScriptErrorNoError") { d.oldContent = scriptEdit.text; - } else if (scriptError == "ScriptErrorInvalidScript") { - content.ToolTip.show(qsTr("The script has not been deployed because it contains errors.")) - } - - errorModel.update(errors); - } - } - onEditScriptReply: { - print("edit reply", id, d.callId) - if (id == d.callId) { - d.callId = -1; - if (scriptError == "ScriptErrorNoError") { - d.oldContent = scriptEdit.text; - } else if (scriptError == "ScriptErrorInvalidScript") { - content.ToolTip.show(qsTr("The script has not been deployed because it contains errors.")) + infoPane.hide(); + errorPane.hide(); + } else if (scriptError === "ScriptErrorInvalidScript") { + errorPane.show(); } errorModel.update(errors) } } + onFetchScriptReply: { if (id == d.callId && scriptError == "ScriptErrorNoError") { d.callId = -1; - scriptEdit.text = content; d.oldContent = content; + + if (autoSaver.cachedContent.length > 0 && autoSaver.cachedContent !== content) { + console.log("autosaved version available!"); + scriptEdit.text = autoSaver.cachedContent; + infoPane.show(); + } else { + scriptEdit.text = content; + } + autoSaver.active = true; } } onRenameScriptReply: { @@ -191,8 +196,30 @@ Page { // TODO: Make this a SplitView when we can use Qt 5.13 ColumnLayout { id: content + spacing: 0 anchors.fill: parent + InfoPane { + id: infoPane + Layout.fillWidth: true + text: qsTr("An autosaved version of this script has been loaded. Deploy to store this version or reload to restore the deployed version.") + buttonText: qsTr("Reload") + z: 1 + onButtonClicked: { + scriptEdit.text = d.oldContent + infoPane.hide(); + errorPane.hide(); + errorModel.update([]) + } + } + + InfoPane { + id: errorPane + Layout.fillWidth: true + color: "red" + text: qsTr("The script has not been deployed because it contains errors.") + } + Flickable { id: scriptFlickable Layout.fillHeight: true @@ -236,7 +263,7 @@ Page { } Keys.onPressed: { - print("key", event.key, "Completion box visible:", completionBox.visible) +// print("key", event.key, "Completion box visible:", completionBox.visible) // Things to happen only when we're not autocompleting if (!completionBox.visible) { switch (event.key) {