Merge PR #465: Add support for autosaving in the script editor
This commit is contained in:
commit
4a6f52271e
@ -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");
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
114
libnymea-app/scripting/scriptautosaver.cpp
Normal file
114
libnymea-app/scripting/scriptautosaver.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
58
libnymea-app/scripting/scriptautosaver.h
Normal file
58
libnymea-app/scripting/scriptautosaver.h
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
93
nymea-app/ui/components/InfoPane.qml
Normal file
93
nymea-app/ui/components/InfoPane.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user