Merge PR #465: Add support for autosaving in the script editor

This commit is contained in:
Jenkins nymea 2020-11-28 21:09:16 +01:00
commit 4a6f52271e
11 changed files with 388 additions and 71 deletions

View File

@ -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"
@ -310,6 +311,7 @@ void registerQmlTypes() {
qmlRegisterType<ScriptSyntaxHighlighter>(uri, 1, 0, "ScriptSyntaxHighlighter");
qmlRegisterType<CodeCompletion>(uri, 1, 0, "CodeCompletion");
qmlRegisterUncreatableType<CompletionProxyModel>(uri, 1, 0, "CompletionModel", "Get it from ScriptSyntaxHighlighter");
qmlRegisterType<ScriptAutoSaver>(uri, 1, 0, "ScriptAutoSaver");
qmlRegisterType<UserManager>(uri, 1, 0, "UserManager");
qmlRegisterUncreatableType<UserInfo>(uri, 1, 0, "UserInfo", "Get it from UserManager");

View File

@ -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 \

View File

@ -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;
}

View File

@ -0,0 +1,114 @@
#include "scriptautosaver.h"
#include <QStandardPaths>
#include <QDir>
#include <QDebug>
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();
}
}

View File

@ -0,0 +1,58 @@
#ifndef SCRIPTAUTOSAVER_H
#define SCRIPTAUTOSAVER_H
#include <QObject>
#include <QUuid>
#include <QQmlParserStatus>
#include <QFile>
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

View File

@ -222,5 +222,6 @@
<file>ui/components/UpdateStatusIcon.qml</file>
<file>ui/magic/WriteNfcTagPage.qml</file>
<file>ui/components/MediaPlayer.qml</file>
<file>ui/components/InfoPane.qml</file>
</qresource>
</RCC>

View File

@ -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

View File

@ -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

View File

@ -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()
}
}
}
}
}

View File

@ -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

View File

@ -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) {