Merge PR #269: Add a script editor for nymea scripts
This commit is contained in:
commit
106c2eabdb
@ -21,6 +21,7 @@
|
|||||||
#include "engine.h"
|
#include "engine.h"
|
||||||
|
|
||||||
#include "rulemanager.h"
|
#include "rulemanager.h"
|
||||||
|
#include "scriptmanager.h"
|
||||||
#include "logmanager.h"
|
#include "logmanager.h"
|
||||||
#include "tagsmanager.h"
|
#include "tagsmanager.h"
|
||||||
#include "configuration/nymeaconfiguration.h"
|
#include "configuration/nymeaconfiguration.h"
|
||||||
@ -39,6 +40,7 @@ Engine::Engine(QObject *parent) :
|
|||||||
m_jsonRpcClient(new JsonRpcClient(m_connection, this)),
|
m_jsonRpcClient(new JsonRpcClient(m_connection, this)),
|
||||||
m_deviceManager(new DeviceManager(m_jsonRpcClient, this)),
|
m_deviceManager(new DeviceManager(m_jsonRpcClient, this)),
|
||||||
m_ruleManager(new RuleManager(m_jsonRpcClient, this)),
|
m_ruleManager(new RuleManager(m_jsonRpcClient, this)),
|
||||||
|
m_scriptManager(new ScriptManager(m_jsonRpcClient, this)),
|
||||||
m_logManager(new LogManager(m_jsonRpcClient, this)),
|
m_logManager(new LogManager(m_jsonRpcClient, this)),
|
||||||
m_tagsManager(new TagsManager(m_jsonRpcClient, this)),
|
m_tagsManager(new TagsManager(m_jsonRpcClient, this)),
|
||||||
m_nymeaConfiguration(new NymeaConfiguration(m_jsonRpcClient, this)),
|
m_nymeaConfiguration(new NymeaConfiguration(m_jsonRpcClient, this)),
|
||||||
@ -80,6 +82,11 @@ RuleManager *Engine::ruleManager() const
|
|||||||
return m_ruleManager;
|
return m_ruleManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScriptManager *Engine::scriptManager() const
|
||||||
|
{
|
||||||
|
return m_scriptManager;
|
||||||
|
}
|
||||||
|
|
||||||
TagsManager *Engine::tagsManager() const
|
TagsManager *Engine::tagsManager() const
|
||||||
{
|
{
|
||||||
return m_tagsManager;
|
return m_tagsManager;
|
||||||
@ -144,6 +151,7 @@ void Engine::onDeviceManagerFetchingChanged()
|
|||||||
{
|
{
|
||||||
if (!m_deviceManager->fetchingData()) {
|
if (!m_deviceManager->fetchingData()) {
|
||||||
m_ruleManager->init();
|
m_ruleManager->init();
|
||||||
|
m_scriptManager->init();
|
||||||
m_nymeaConfiguration->init();
|
m_nymeaConfiguration->init();
|
||||||
m_systemController->init();
|
m_systemController->init();
|
||||||
if (m_jsonRpcClient->ensureServerVersion("1.7")) {
|
if (m_jsonRpcClient->ensureServerVersion("1.7")) {
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
#include "wifisetup/bluetoothdiscovery.h"
|
#include "wifisetup/bluetoothdiscovery.h"
|
||||||
|
|
||||||
class RuleManager;
|
class RuleManager;
|
||||||
|
class ScriptManager;
|
||||||
class LogManager;
|
class LogManager;
|
||||||
class TagsManager;
|
class TagsManager;
|
||||||
class NymeaConfiguration;
|
class NymeaConfiguration;
|
||||||
@ -41,6 +42,7 @@ class Engine : public QObject
|
|||||||
Q_PROPERTY(NymeaConnection* connection READ connection CONSTANT)
|
Q_PROPERTY(NymeaConnection* connection READ connection CONSTANT)
|
||||||
Q_PROPERTY(DeviceManager* deviceManager READ deviceManager CONSTANT)
|
Q_PROPERTY(DeviceManager* deviceManager READ deviceManager CONSTANT)
|
||||||
Q_PROPERTY(RuleManager* ruleManager READ ruleManager CONSTANT)
|
Q_PROPERTY(RuleManager* ruleManager READ ruleManager CONSTANT)
|
||||||
|
Q_PROPERTY(ScriptManager* scriptManager READ scriptManager CONSTANT)
|
||||||
Q_PROPERTY(TagsManager* tagsManager READ tagsManager CONSTANT)
|
Q_PROPERTY(TagsManager* tagsManager READ tagsManager CONSTANT)
|
||||||
Q_PROPERTY(JsonRpcClient* jsonRpcClient READ jsonRpcClient CONSTANT)
|
Q_PROPERTY(JsonRpcClient* jsonRpcClient READ jsonRpcClient CONSTANT)
|
||||||
Q_PROPERTY(NymeaConfiguration* nymeaConfiguration READ nymeaConfiguration CONSTANT)
|
Q_PROPERTY(NymeaConfiguration* nymeaConfiguration READ nymeaConfiguration CONSTANT)
|
||||||
@ -55,6 +57,7 @@ public:
|
|||||||
NymeaConnection *connection() const;
|
NymeaConnection *connection() const;
|
||||||
DeviceManager *deviceManager() const;
|
DeviceManager *deviceManager() const;
|
||||||
RuleManager *ruleManager() const;
|
RuleManager *ruleManager() const;
|
||||||
|
ScriptManager *scriptManager() const;
|
||||||
TagsManager *tagsManager() const;
|
TagsManager *tagsManager() const;
|
||||||
JsonRpcClient *jsonRpcClient() const;
|
JsonRpcClient *jsonRpcClient() const;
|
||||||
LogManager *logManager() const;
|
LogManager *logManager() const;
|
||||||
@ -68,6 +71,7 @@ private:
|
|||||||
JsonRpcClient *m_jsonRpcClient;
|
JsonRpcClient *m_jsonRpcClient;
|
||||||
DeviceManager *m_deviceManager;
|
DeviceManager *m_deviceManager;
|
||||||
RuleManager *m_ruleManager;
|
RuleManager *m_ruleManager;
|
||||||
|
ScriptManager *m_scriptManager;
|
||||||
LogManager *m_logManager;
|
LogManager *m_logManager;
|
||||||
TagsManager *m_tagsManager;
|
TagsManager *m_tagsManager;
|
||||||
NymeaConfiguration *m_nymeaConfiguration;
|
NymeaConfiguration *m_nymeaConfiguration;
|
||||||
|
|||||||
@ -68,6 +68,12 @@
|
|||||||
#include "configuration/networkmanager.h"
|
#include "configuration/networkmanager.h"
|
||||||
#include "types/networkdevices.h"
|
#include "types/networkdevices.h"
|
||||||
#include "types/networkdevice.h"
|
#include "types/networkdevice.h"
|
||||||
|
#include "scriptsyntaxhighlighter.h"
|
||||||
|
#include "scriptmanager.h"
|
||||||
|
#include "scripting/codecompletion.h"
|
||||||
|
#include "scripting/completionmodel.h"
|
||||||
|
#include "types/script.h"
|
||||||
|
#include "types/scripts.h"
|
||||||
|
|
||||||
#include <QtQml/qqml.h>
|
#include <QtQml/qqml.h>
|
||||||
|
|
||||||
@ -230,6 +236,13 @@ void registerQmlTypes() {
|
|||||||
qmlRegisterUncreatableType<NetworkDevice>(uri, 1, 0, "NetworkDevice", "Get it from NetworkDevices");
|
qmlRegisterUncreatableType<NetworkDevice>(uri, 1, 0, "NetworkDevice", "Get it from NetworkDevices");
|
||||||
qmlRegisterUncreatableType<WiredNetworkDevice>(uri, 1, 0, "WiredNetworkDevice", "Get it from NetworkDevices");
|
qmlRegisterUncreatableType<WiredNetworkDevice>(uri, 1, 0, "WiredNetworkDevice", "Get it from NetworkDevices");
|
||||||
qmlRegisterUncreatableType<WirelessNetworkDevice>(uri, 1, 0, "WirelessNetworkDevice", "Get it from NetworkDevices");
|
qmlRegisterUncreatableType<WirelessNetworkDevice>(uri, 1, 0, "WirelessNetworkDevice", "Get it from NetworkDevices");
|
||||||
|
|
||||||
|
qmlRegisterUncreatableType<ScriptManager>(uri, 1, 0, "ScriptManager", "Get it from Engine");
|
||||||
|
qmlRegisterUncreatableType<Scripts>(uri, 1, 0, "Scripts", "Getit from ScriptManager");
|
||||||
|
qmlRegisterUncreatableType<Script>(uri, 1, 0, "Script", "Getit from Scripts");
|
||||||
|
qmlRegisterType<ScriptSyntaxHighlighter>(uri, 1, 0, "ScriptSyntaxHighlighter");
|
||||||
|
qmlRegisterType<CodeCompletion>(uri, 1, 0, "CodeCompletion");
|
||||||
|
qmlRegisterUncreatableType<CompletionProxyModel>(uri, 1, 0, "CompletionModel", "Get it from ScriptSyntaxHighlighter");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // LIBNYMEAAPPCORE_H
|
#endif // LIBNYMEAAPPCORE_H
|
||||||
|
|||||||
@ -15,7 +15,7 @@ include(../nymea-remoteproxy/libnymea-remoteproxyclient/libnymea-remoteproxyclie
|
|||||||
|
|
||||||
|
|
||||||
QT -= gui
|
QT -= gui
|
||||||
QT += network websockets bluetooth charts
|
QT += network websockets bluetooth charts quick
|
||||||
|
|
||||||
LIBS += -lssl -lcrypto
|
LIBS += -lssl -lcrypto
|
||||||
|
|
||||||
@ -48,6 +48,10 @@ SOURCES += \
|
|||||||
devicediscovery.cpp \
|
devicediscovery.cpp \
|
||||||
models/packagesfiltermodel.cpp \
|
models/packagesfiltermodel.cpp \
|
||||||
models/taglistmodel.cpp \
|
models/taglistmodel.cpp \
|
||||||
|
scripting/codecompletion.cpp \
|
||||||
|
scripting/completionmodel.cpp \
|
||||||
|
scriptmanager.cpp \
|
||||||
|
scriptsyntaxhighlighter.cpp \
|
||||||
vendorsproxy.cpp \
|
vendorsproxy.cpp \
|
||||||
pluginsproxy.cpp \
|
pluginsproxy.cpp \
|
||||||
interfacesmodel.cpp \
|
interfacesmodel.cpp \
|
||||||
@ -111,6 +115,10 @@ HEADERS += \
|
|||||||
devicediscovery.h \
|
devicediscovery.h \
|
||||||
models/packagesfiltermodel.h \
|
models/packagesfiltermodel.h \
|
||||||
models/taglistmodel.h \
|
models/taglistmodel.h \
|
||||||
|
scripting/codecompletion.h \
|
||||||
|
scripting/completionmodel.h \
|
||||||
|
scriptmanager.h \
|
||||||
|
scriptsyntaxhighlighter.h \
|
||||||
vendorsproxy.h \
|
vendorsproxy.h \
|
||||||
pluginsproxy.h \
|
pluginsproxy.h \
|
||||||
interfacesmodel.h \
|
interfacesmodel.h \
|
||||||
|
|||||||
752
libnymea-app-core/scripting/codecompletion.cpp
Normal file
752
libnymea-app-core/scripting/codecompletion.cpp
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
#include "codecompletion.h"
|
||||||
|
|
||||||
|
#include "engine.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
|
CodeCompletion::CodeCompletion(QObject *parent):
|
||||||
|
QObject(parent)
|
||||||
|
{
|
||||||
|
m_classes.insert("Item", ClassInfo("Item", {"id"}));
|
||||||
|
m_classes.insert("DeviceAction", ClassInfo("DeviceAction", {"id", "deviceId", "actionTypeId", "actionName"}, {"execute"}));
|
||||||
|
m_classes.insert("DeviceState", ClassInfo("DeviceState", {"id", "deviceId", "stateTypeId", "stateName", "value"}, {}, {"onValueChanged"}));
|
||||||
|
m_classes.insert("DeviceEvent", ClassInfo("DeviceEvent", {"id", "deviceId", "eventTypeId", "eventName"}, {}, {"onTriggered"}));
|
||||||
|
m_classes.insert("Timer", ClassInfo("Timer", {"id", "interval", "running", "repeat"}, {"start", "stop"}, {"onTriggered"}));
|
||||||
|
m_classes.insert("Alarm", ClassInfo("Alarm", {"id", "time", "endTime", "weekDays", "active"}, {}, {"onTriggered", "onActiveChanged"}));
|
||||||
|
m_classes.insert("PropertyAnimation", ClassInfo("PropertyAnimation", {"id", "target", "targets", "property", "properties", "value", "from", "to", "easing", "exclude", "duration", "alwaysRunToEnd", "loops", "paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"}));
|
||||||
|
m_classes.insert("ColorAnimation", ClassInfo("ColorAnimation", {"id", "target", "targets", "property", "properties", "value", "from", "to", "easing", "exclude", "duration", "alwaysRunToEnd", "loops", "paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"}));
|
||||||
|
m_classes.insert("SequentialAnimation", ClassInfo("SequentialAnimation", {"id", "alwaysRunToEnd", "loops", "paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"}));
|
||||||
|
m_classes.insert("ParallelAnimation", ClassInfo("ParallelAnimation", {"id", "alwaysRunToEnd", "loops", "paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"}));
|
||||||
|
m_classes.insert("PauseAnimation", ClassInfo("PauseAnimation", {"id", "duration", "alwaysRunToEnd", "loops", "paused", "running"}, {"start", "stop", "pause", "resume", "complete"}, {"onStarted", "onStopped", "onFinished", "onRunningChanged"}));
|
||||||
|
m_classes.insert("ListModel", ClassInfo("ListModel", {}, {"clear", "append"}, {}));
|
||||||
|
m_classes.insert("ListElement", ClassInfo("ListElement", {}, {}, {}));
|
||||||
|
m_classes.insert("Repeater", ClassInfo("Repeater", {"model", "delegate", "count"}, {"itemAt"}, {"onCountChanged"}));
|
||||||
|
|
||||||
|
m_attachedClasses.insert("Component", ClassInfo("Component", {}, {}, {"onCompleted", "onDestruction", "onDestroyed"}));
|
||||||
|
m_attachedClasses.insert("Alarm", ClassInfo("Alarm", {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", "AllDays"}, {}, {}));
|
||||||
|
m_attachedClasses.insert("Animation", ClassInfo("Animation", {"Infinite"}, {}, {}));
|
||||||
|
|
||||||
|
m_genericSyntax.insert("property", "property ");
|
||||||
|
m_genericSyntax.insert("function", "function ");
|
||||||
|
|
||||||
|
m_genericJsSyntax.insert("for", "for");
|
||||||
|
m_genericJsSyntax.insert("var", "var");
|
||||||
|
m_genericJsSyntax.insert("while", "while ");
|
||||||
|
m_genericJsSyntax.insert("do", "do ");
|
||||||
|
m_genericJsSyntax.insert("if", "if ");
|
||||||
|
m_genericJsSyntax.insert("else", "else ");
|
||||||
|
m_genericJsSyntax.insert("print", "print");
|
||||||
|
|
||||||
|
m_jsClasses.insert("console", ClassInfo("console", {}, {"log", "warn"}));
|
||||||
|
m_jsClasses.insert("JSON", ClassInfo("JSON", {}, {"stringify", "parse", "hasOwnProperty", "isPrototypeOf", "toString", "valueOf", "toLocaleString", "propertyIsEnumerable"}));
|
||||||
|
|
||||||
|
m_model = new CompletionModel(this);
|
||||||
|
m_proxy = new CompletionProxyModel(m_model, this);
|
||||||
|
connect(m_proxy, &CompletionProxyModel::filterChanged, this, &CodeCompletion::currentWordChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
Engine *CodeCompletion::engine() const
|
||||||
|
{
|
||||||
|
return m_engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::setEngine(Engine *engine)
|
||||||
|
{
|
||||||
|
if (m_engine != engine) {
|
||||||
|
m_engine = engine;
|
||||||
|
emit engineChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickTextDocument *CodeCompletion::document() const
|
||||||
|
{
|
||||||
|
return m_document;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::setDocument(QQuickTextDocument *document)
|
||||||
|
{
|
||||||
|
if (m_document != document) {
|
||||||
|
m_document = document;
|
||||||
|
emit documentChanged();
|
||||||
|
m_cursor = QTextCursor(m_document->textDocument());
|
||||||
|
emit cursorPositionChanged();
|
||||||
|
|
||||||
|
connect(m_document->textDocument(), &QTextDocument::cursorPositionChanged, this, [this](const QTextCursor &cursor){
|
||||||
|
m_cursor = cursor;
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int CodeCompletion::cursorPosition() const
|
||||||
|
{
|
||||||
|
return m_cursor.position();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::setCursorPosition(int position)
|
||||||
|
{
|
||||||
|
// This is a bit tricky: As our cursor works on the same textDocument as the view,
|
||||||
|
// our cursor will already have the position set to the new one by the time we
|
||||||
|
// receive the update from the View when the document is changed.
|
||||||
|
// But we can't just connect to our cursor's updates as that will miss out events
|
||||||
|
// generated in the UI without changing the document (e.g. move cursor with kbd/mouse)
|
||||||
|
|
||||||
|
if (m_cursor.position() != position) {
|
||||||
|
m_cursor.setPosition(position);
|
||||||
|
// NOTE: Don't emit cursorPositionChanged here, it will break selections
|
||||||
|
// because the view thinks we've edited the document.
|
||||||
|
// If we actually edit the document, the view will sync up automatically
|
||||||
|
// through the document. So we must *only* emit cursorPositionChanged when
|
||||||
|
// we actually want to move it without changing the document.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CodeCompletion::currentWord() const
|
||||||
|
{
|
||||||
|
return m_proxy->filter();
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletionProxyModel *CodeCompletion::model() const
|
||||||
|
{
|
||||||
|
return m_proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::update()
|
||||||
|
{
|
||||||
|
if (!m_engine || !m_document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int lastUpdatePos = -1;
|
||||||
|
if (lastUpdatePos == m_cursor.position()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUpdatePos = m_cursor.position();
|
||||||
|
|
||||||
|
QTextCursor tmp = m_cursor;
|
||||||
|
tmp.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
QString blockText = tmp.selectedText();
|
||||||
|
|
||||||
|
QList<CompletionModel::Entry> entries;
|
||||||
|
|
||||||
|
QRegExp deviceIdExp(".*deviceId: \"[a-zA-Z0-9- ]*");
|
||||||
|
if (deviceIdExp.exactMatch(blockText)) {
|
||||||
|
for (int i = 0; i < m_engine->deviceManager()->devices()->rowCount(); i++) {
|
||||||
|
Device *dev = m_engine->deviceManager()->devices()->get(i);
|
||||||
|
entries.append(CompletionModel::Entry(dev->id().toString() + "\" // " + dev->name(), dev->name(), "thing", dev->deviceClass()->interfaces().join(",")));
|
||||||
|
}
|
||||||
|
blockText.remove(QRegExp(".*deviceId: \""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText, false);
|
||||||
|
emit hint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp stateTypeIdExp(".*stateTypeId: \"[a-zA-Z0-9-]*");
|
||||||
|
if (stateTypeIdExp.exactMatch(blockText)) {
|
||||||
|
BlockInfo info = getBlockInfo(m_cursor.position());
|
||||||
|
if (!info.properties.contains("deviceId")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString deviceId = info.properties.value("deviceId");
|
||||||
|
|
||||||
|
qDebug() << "selected deviceId" << deviceId;
|
||||||
|
Device *device = m_engine->deviceManager()->devices()->getDevice(deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) {
|
||||||
|
StateType *stateType = device->deviceClass()->stateTypes()->get(i);
|
||||||
|
entries.append(CompletionModel::Entry(stateType->id().toString() + "\" // " + stateType->name(), stateType->name(), "stateType"));
|
||||||
|
}
|
||||||
|
blockText.remove(QRegExp(".*stateTypeId: \""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
emit hint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp stateNameExp(".*stateName: \"[a-zA-Z0-9-]*");
|
||||||
|
qDebug() << "block text" << blockText << stateNameExp.exactMatch(blockText);
|
||||||
|
if (stateNameExp.exactMatch(blockText)) {
|
||||||
|
BlockInfo info = getBlockInfo(m_cursor.position());
|
||||||
|
qDebug() << "stateName block info" << info.name << info.properties;
|
||||||
|
if (!info.properties.contains("deviceId")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString deviceId = info.properties.value("deviceId");
|
||||||
|
|
||||||
|
qDebug() << "selected deviceId" << deviceId;
|
||||||
|
Device *device = m_engine->deviceManager()->devices()->getDevice(deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < device->deviceClass()->stateTypes()->rowCount(); i++) {
|
||||||
|
StateType *stateType = device->deviceClass()->stateTypes()->get(i);
|
||||||
|
entries.append(CompletionModel::Entry(stateType->name() + "\"", stateType->name(), "stateType"));
|
||||||
|
}
|
||||||
|
blockText.remove(QRegExp(".*stateName: \""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
emit hint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp actionTypeIdExp(".*actionTypeId: \"[a-zA-Z0-9-]*");
|
||||||
|
if (actionTypeIdExp.exactMatch(blockText)) {
|
||||||
|
BlockInfo info = getBlockInfo(m_cursor.position());
|
||||||
|
if (!info.properties.contains("deviceId")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString deviceId = info.properties.value("deviceId");
|
||||||
|
|
||||||
|
qDebug() << "selected deviceId" << deviceId;
|
||||||
|
Device *device = m_engine->deviceManager()->devices()->getDevice(deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < device->deviceClass()->actionTypes()->rowCount(); i++) {
|
||||||
|
ActionType *actionType = device->deviceClass()->actionTypes()->get(i);
|
||||||
|
entries.append(CompletionModel::Entry(actionType->id().toString() + "\" // " + actionType->name(), actionType->name(), "actionType"));
|
||||||
|
}
|
||||||
|
blockText.remove(QRegExp(".*actionTypeId: \""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
emit hint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp actionNameExp(".*actionName: \"[a-zA-Z0-9-]*");
|
||||||
|
if (actionNameExp.exactMatch(blockText)) {
|
||||||
|
BlockInfo info = getBlockInfo(m_cursor.position());
|
||||||
|
if (!info.properties.contains("deviceId")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString deviceId = info.properties.value("deviceId");
|
||||||
|
|
||||||
|
qDebug() << "selected deviceId" << deviceId;
|
||||||
|
Device *device = m_engine->deviceManager()->devices()->getDevice(deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < device->deviceClass()->actionTypes()->rowCount(); i++) {
|
||||||
|
ActionType *actionType = device->deviceClass()->actionTypes()->get(i);
|
||||||
|
entries.append(CompletionModel::Entry(actionType->name() + "\"", actionType->name(), "actionType"));
|
||||||
|
}
|
||||||
|
blockText.remove(QRegExp(".*actionName: \""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
emit hint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp eventTypeIdExp(".*eventTypeId: \"[a-zA-Z0-9-]*");
|
||||||
|
if (eventTypeIdExp.exactMatch(blockText)) {
|
||||||
|
BlockInfo info = getBlockInfo(m_cursor.position());
|
||||||
|
if (!info.properties.contains("deviceId")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString deviceId = info.properties.value("deviceId");
|
||||||
|
|
||||||
|
Device *device = m_engine->deviceManager()->devices()->getDevice(deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < device->deviceClass()->eventTypes()->rowCount(); i++) {
|
||||||
|
EventType *eventType = device->deviceClass()->eventTypes()->get(i);
|
||||||
|
entries.append(CompletionModel::Entry(eventType->id().toString() + "\" // " + eventType->name(), eventType->name(), "eventType"));
|
||||||
|
}
|
||||||
|
blockText.remove(QRegExp(".*eventTypeId: \""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
emit hint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp eventNameExp(".*eventName: \"[a-zA-Z0-9-]*");
|
||||||
|
if (eventNameExp.exactMatch(blockText)) {
|
||||||
|
BlockInfo info = getBlockInfo(m_cursor.position());
|
||||||
|
if (!info.properties.contains("deviceId")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QString deviceId = info.properties.value("deviceId");
|
||||||
|
|
||||||
|
Device *device = m_engine->deviceManager()->devices()->getDevice(deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < device->deviceClass()->eventTypes()->rowCount(); i++) {
|
||||||
|
EventType *eventType = device->deviceClass()->eventTypes()->get(i);
|
||||||
|
entries.append(CompletionModel::Entry(eventType->name() + "\"", eventType->name(), "eventType"));
|
||||||
|
}
|
||||||
|
blockText.remove(QRegExp(".*eventName: \""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
emit hint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp importExp("imp(o|or)?");
|
||||||
|
if (importExp.exactMatch(blockText)) {
|
||||||
|
entries.append(CompletionModel::Entry("import ", "import", "keyword", ""));
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp importExp2("import [a-zA-Z]*");
|
||||||
|
if (importExp2.exactMatch(blockText)) {
|
||||||
|
entries.append(CompletionModel::Entry("QtQuick 2.0"));
|
||||||
|
entries.append(CompletionModel::Entry("nymea 1.0"));
|
||||||
|
m_model->update(entries);
|
||||||
|
blockText.remove("import ");
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp rValueExp(" *[\\.a-zA-Z0-0]+[^id]:[ a-zA-Z0-0]*");
|
||||||
|
if (rValueExp.exactMatch(blockText)) {
|
||||||
|
QTextCursor tmp = m_cursor;
|
||||||
|
tmp.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
|
||||||
|
QString word = tmp.selectedText();
|
||||||
|
|
||||||
|
tmp.movePosition(QTextCursor::PreviousWord, QTextCursor::MoveAnchor, 2);
|
||||||
|
tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||||
|
QString previousWord = tmp.selectedText();
|
||||||
|
|
||||||
|
if (previousWord.isEmpty()) {
|
||||||
|
m_model->update({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "rValue" << previousWord << word;
|
||||||
|
entries.append(getIds());
|
||||||
|
foreach (const QString &s, m_jsClasses.keys()) {
|
||||||
|
entries.append(CompletionModel::Entry(s, s, "type"));
|
||||||
|
}
|
||||||
|
foreach (const QString &s, m_attachedClasses.keys()) {
|
||||||
|
entries.append(CompletionModel::Entry(s, s, "type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(word);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp dotExp(".*[a-zA-Z0-9]+\\.[a-zA-Z0-9]*");
|
||||||
|
if (dotExp.exactMatch(blockText)) {
|
||||||
|
QString id = blockText;
|
||||||
|
id.remove(QRegExp(".* ")).remove(QRegExp("\\.[a-zA-Z0-9]*"));
|
||||||
|
QString type = getIdTypes().value(id);
|
||||||
|
qDebug() << "dot expression:" << id << type;
|
||||||
|
// Classes
|
||||||
|
foreach (const QString &property, m_classes.value(type).properties) {
|
||||||
|
entries.append(CompletionModel::Entry(property, property, "property"));
|
||||||
|
}
|
||||||
|
foreach (const QString &method, m_classes.value(type).methods) {
|
||||||
|
entries.append(CompletionModel::Entry(method + "(", method, "method", "", ")"));
|
||||||
|
}
|
||||||
|
// Attached classes/properties
|
||||||
|
foreach (const QString &property, m_attachedClasses.value(id).properties) {
|
||||||
|
entries.append(CompletionModel::Entry(property, property, "property"));
|
||||||
|
}
|
||||||
|
foreach (const QString &method, m_attachedClasses.value(id).methods) {
|
||||||
|
entries.append(CompletionModel::Entry(method + "(", method, "method", "", ")"));
|
||||||
|
}
|
||||||
|
foreach (const QString &event, m_attachedClasses.value(id).events) {
|
||||||
|
entries.append(CompletionModel::Entry(event + ": ", event, "event"));
|
||||||
|
}
|
||||||
|
// JS global objects
|
||||||
|
foreach (const QString &property, m_jsClasses.value(id).properties) {
|
||||||
|
entries.append(CompletionModel::Entry(property, property, "property"));
|
||||||
|
}
|
||||||
|
foreach (const QString &method, m_jsClasses.value(id).methods) {
|
||||||
|
entries.append(CompletionModel::Entry(method + "(", method, "method", "", ")"));
|
||||||
|
}
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText.remove(QRegExp(".*\\.")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we in a JS block?
|
||||||
|
int pos = m_cursor.position();
|
||||||
|
BlockInfo jsBlock = getBlockInfo(pos);
|
||||||
|
bool isImperative = jsBlock.name.endsWith(":") || jsBlock.name.endsWith("()");
|
||||||
|
bool atStart = false;
|
||||||
|
while (!isImperative && jsBlock.valid && !atStart) {
|
||||||
|
qDebug() << "is imperative block?" << isImperative << jsBlock.name << "blockText" << blockText;
|
||||||
|
BlockInfo tmp = getBlockInfo(jsBlock.start - 1);
|
||||||
|
if (tmp.valid) {
|
||||||
|
jsBlock = tmp;
|
||||||
|
isImperative = jsBlock.name.endsWith(":") || jsBlock.name.endsWith("()");
|
||||||
|
} else {
|
||||||
|
atStart = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isImperative) {
|
||||||
|
qDebug() << "Is imperative!";
|
||||||
|
// Starting a new expression?
|
||||||
|
QRegExp newExpressionExp("(.*; [a-zA-Z0-9]*| *[a-zA-Z0-9]*)");
|
||||||
|
if (newExpressionExp.exactMatch(blockText)) {
|
||||||
|
// Add generic qml syntax
|
||||||
|
foreach (const QString &s, m_genericJsSyntax.keys()) {
|
||||||
|
entries.append(CompletionModel::Entry(m_genericJsSyntax.value(s), s, "keyword", ""));
|
||||||
|
}
|
||||||
|
// Add js global objects
|
||||||
|
foreach (const QString &s, m_jsClasses.keys()) {
|
||||||
|
entries.append(CompletionModel::Entry(s, s, "type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(getIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_model->update(entries);
|
||||||
|
m_proxy->setFilter(blockText.remove(QRegExp(".* ")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegExp lValueStartExp(" *[a-zA-Z0-9]*");
|
||||||
|
if (lValueStartExp.exactMatch(blockText)) {
|
||||||
|
BlockInfo blockInfo = getBlockInfo(m_cursor.position());
|
||||||
|
|
||||||
|
// If we're inside a class, add properties
|
||||||
|
qDebug() << "Block name" << blockInfo.name;
|
||||||
|
|
||||||
|
if (!blockInfo.name.isEmpty()) {
|
||||||
|
foreach (const QString &s, m_classes.value(blockInfo.name).properties) {
|
||||||
|
if (!blockInfo.properties.contains(s)) {
|
||||||
|
entries.append(CompletionModel::Entry(s + ": ", s, "property"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (const QString &s, m_classes.value(blockInfo.name).events) {
|
||||||
|
if (!blockInfo.properties.contains(s)) {
|
||||||
|
entries.append(CompletionModel::Entry(s + ": ", s, "event"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always append class names
|
||||||
|
foreach (const QString &s, m_classes.keys()) {
|
||||||
|
entries.append(CompletionModel::Entry(s + " {", s, "type", "", "}"));
|
||||||
|
}
|
||||||
|
// Always append attached class names
|
||||||
|
foreach (const QString &s, m_attachedClasses.keys()) {
|
||||||
|
entries.append(CompletionModel::Entry(s, s, "type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add generic qml syntax
|
||||||
|
foreach (const QString &s, m_genericSyntax.keys()) {
|
||||||
|
entries.append(CompletionModel::Entry(m_genericSyntax.value(s), s, "keyword", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_model->update(entries);
|
||||||
|
blockText.remove(QRegExp(".* "));
|
||||||
|
m_proxy->setFilter(blockText);
|
||||||
|
qDebug() << "Model has" << m_model->rowCount() << "Filtered:" << m_proxy->rowCount() << "filter:" << blockText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
m_model->update({});
|
||||||
|
m_proxy->setFilter(QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeCompletion::BlockInfo CodeCompletion::getBlockInfo(int position) const
|
||||||
|
{
|
||||||
|
BlockInfo info;
|
||||||
|
|
||||||
|
QTextCursor blockStart = m_document->textDocument()->find("{", position, QTextDocument::FindBackward);
|
||||||
|
QTextCursor blockEnd = m_document->textDocument()->find("}", position, QTextDocument::FindBackward);
|
||||||
|
while (blockEnd.position() > blockStart.position() && !blockStart.isNull()) {
|
||||||
|
blockStart = m_document->textDocument()->find("{", blockStart, QTextDocument::FindBackward);
|
||||||
|
blockEnd = m_document->textDocument()->find("}", blockEnd, QTextDocument::FindBackward);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockStart.isNull()) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.start = blockStart.position();
|
||||||
|
info.end = m_document->textDocument()->find("}", position).position();
|
||||||
|
info.valid = true;
|
||||||
|
|
||||||
|
qDebug() << "block name" << blockStart.block().text();
|
||||||
|
info.name = blockStart.block().text();
|
||||||
|
info.name.remove(QRegExp(" *\\{ *"));
|
||||||
|
qDebug() << "stripped klammer" << info.name;
|
||||||
|
while (info.name.contains(" ")) {
|
||||||
|
info.name.remove(QRegExp(".* "));
|
||||||
|
}
|
||||||
|
qDebug() << "final name" << info.name;
|
||||||
|
|
||||||
|
int childBlocks = 0;
|
||||||
|
while (!blockStart.isNull() && blockStart.position() < position) {
|
||||||
|
QString line = blockStart.block().text();
|
||||||
|
if (line.endsWith("{")) {
|
||||||
|
childBlocks++;
|
||||||
|
if (!blockStart.movePosition(QTextCursor::NextBlock)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.trimmed().startsWith("}")) {
|
||||||
|
childBlocks--;
|
||||||
|
if (!blockStart.movePosition(QTextCursor::NextBlock)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// \n
|
||||||
|
if (childBlocks > 1) { // Skip all stuff in child blocks
|
||||||
|
blockStart.movePosition(QTextCursor::NextBlock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (const QString &statement, blockStart.block().text().split(";")) {
|
||||||
|
// qDebug() << "Have statement" << statement;
|
||||||
|
QStringList parts = statement.split(":");
|
||||||
|
if (parts.length() != 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString propName = parts.first().trimmed();
|
||||||
|
QString propValue = parts.last().split("//").first().trimmed().remove("\"");
|
||||||
|
// qDebug() << "inserting:" << propName << "->" << propValue;
|
||||||
|
info.properties.insert(propName, propValue);
|
||||||
|
}
|
||||||
|
if (!blockStart.movePosition(QTextCursor::NextBlock)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<CompletionModel::Entry> CodeCompletion::getIds() const
|
||||||
|
{
|
||||||
|
// Find all ids in the doc
|
||||||
|
QList<CompletionModel::Entry> entries;
|
||||||
|
QTextCursor tmp = QTextCursor(m_document->textDocument());
|
||||||
|
while (!tmp.atEnd()) {
|
||||||
|
tmp.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
|
||||||
|
tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||||
|
QString word = tmp.selectedText();
|
||||||
|
if (word == "id") {
|
||||||
|
tmp.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor);
|
||||||
|
tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||||
|
QString idName = tmp.selectedText();
|
||||||
|
entries.append(CompletionModel::Entry(idName, idName, "id", ""));
|
||||||
|
}
|
||||||
|
tmp.movePosition(QTextCursor::NextWord);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<QString, QString> CodeCompletion::getIdTypes() const
|
||||||
|
{
|
||||||
|
QHash<QString, QString> ret;
|
||||||
|
QTextCursor tmp = QTextCursor(m_document->textDocument());
|
||||||
|
while (!tmp.atEnd()) {
|
||||||
|
tmp.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
|
||||||
|
tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||||
|
QString word = tmp.selectedText();
|
||||||
|
if (word == "id") {
|
||||||
|
tmp.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor);
|
||||||
|
tmp.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||||
|
QString idName = tmp.selectedText();
|
||||||
|
BlockInfo info = getBlockInfo(tmp.position());
|
||||||
|
if (!info.name.isEmpty()) {
|
||||||
|
ret.insert(idName, info.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmp.movePosition(QTextCursor::NextWord);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CodeCompletion::openingBlocksBefore(int position) const
|
||||||
|
{
|
||||||
|
int opening = 0;
|
||||||
|
int closing = 0;
|
||||||
|
QTextCursor tmp = m_cursor;
|
||||||
|
tmp.setPosition(position);
|
||||||
|
do {
|
||||||
|
tmp = m_document->textDocument()->find(QRegExp("[{}]"), tmp, QTextDocument::FindBackward);
|
||||||
|
if (tmp.selectedText() == "{")
|
||||||
|
opening++;
|
||||||
|
if (tmp.selectedText() == "}")
|
||||||
|
closing++;
|
||||||
|
} while (!tmp.isNull());
|
||||||
|
|
||||||
|
return opening - closing;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CodeCompletion::closingBlocksAfter(int position) const
|
||||||
|
{
|
||||||
|
int opening = 0;
|
||||||
|
int closing = 0;
|
||||||
|
QTextCursor tmp = m_cursor;
|
||||||
|
tmp.setPosition(position);
|
||||||
|
do {
|
||||||
|
tmp = m_document->textDocument()->find(QRegExp("[{}]"), tmp);
|
||||||
|
if (tmp.selectedText() == "{")
|
||||||
|
opening++;
|
||||||
|
if (tmp.selectedText() == "}")
|
||||||
|
closing++;
|
||||||
|
} while (!tmp.isNull());
|
||||||
|
|
||||||
|
return closing - opening;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::complete(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_proxy->rowCount()) {
|
||||||
|
qWarning() << "Invalid index for completion";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompletionModel::Entry entry = m_proxy->get(index);
|
||||||
|
|
||||||
|
m_cursor.select(QTextCursor::WordUnderCursor);
|
||||||
|
m_cursor.removeSelectedText();
|
||||||
|
qDebug() << "inserting:" << entry.text;
|
||||||
|
m_cursor.insertText(entry.text);
|
||||||
|
|
||||||
|
qDebug() << "inserting after cursor:" << entry.trailingText;
|
||||||
|
insertAfterCursor(entry.trailingText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::newLine()
|
||||||
|
{
|
||||||
|
qDebug() << "Newline" << m_cursor.position();
|
||||||
|
QString line = m_cursor.block().text();
|
||||||
|
|
||||||
|
if (line.endsWith("{") && openingBlocksBefore(m_cursor.position()) > closingBlocksAfter(m_cursor.position())) {
|
||||||
|
m_cursor.insertText("}");
|
||||||
|
m_cursor.movePosition(QTextCursor::PreviousCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString trimmedLine = line;
|
||||||
|
trimmedLine.remove(QRegExp("^[ ]+"));
|
||||||
|
int indent = line.length() - trimmedLine.length();
|
||||||
|
|
||||||
|
m_cursor.insertText(QString("\n").leftJustified(indent + 1, ' '));
|
||||||
|
if (m_cursor.block().previous().text().endsWith("{")) {
|
||||||
|
m_cursor.insertText(" ");
|
||||||
|
if (m_cursor.block().text().trimmed().endsWith("}")) {
|
||||||
|
m_cursor.insertText(QString("\n").leftJustified(indent + 1, ' '));
|
||||||
|
m_cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor, 1);
|
||||||
|
m_cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1);
|
||||||
|
emit cursorPositionChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::indent(int from, int to)
|
||||||
|
{
|
||||||
|
QTextCursor tmp = QTextCursor(m_document->textDocument());
|
||||||
|
tmp.setPosition(from);
|
||||||
|
if (from == to) {
|
||||||
|
tmp.insertText(" ");
|
||||||
|
} else {
|
||||||
|
while (tmp.position() < to) {
|
||||||
|
tmp.insertText(" ");
|
||||||
|
to += 4;
|
||||||
|
if (!tmp.movePosition(QTextCursor::NextBlock)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::unindent(int from, int to)
|
||||||
|
{
|
||||||
|
QTextCursor tmp = QTextCursor(m_document->textDocument());
|
||||||
|
tmp.setPosition(from);
|
||||||
|
tmp.movePosition(QTextCursor::StartOfLine);
|
||||||
|
if (from == to) {
|
||||||
|
if (tmp.block().text().startsWith(" ")) {
|
||||||
|
tmp.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 4);
|
||||||
|
tmp.removeSelectedText();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Make sure all selected lines start with 4 empty spaces before we start editing
|
||||||
|
bool ok = true;
|
||||||
|
while (tmp.position() < to) {
|
||||||
|
if (!tmp.block().text().startsWith(" ")) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!tmp.movePosition(QTextCursor::NextBlock)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
tmp.setPosition(from);
|
||||||
|
tmp.movePosition(QTextCursor::StartOfLine);
|
||||||
|
while (tmp.position() < to) {
|
||||||
|
tmp.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 4);
|
||||||
|
tmp.removeSelectedText();
|
||||||
|
to -= 4;
|
||||||
|
if (!tmp.movePosition(QTextCursor::NextBlock)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::closeBlock()
|
||||||
|
{
|
||||||
|
m_cursor.insertText("}");
|
||||||
|
if (m_cursor.block().text().trimmed() == "}") {
|
||||||
|
unindent(m_cursor.position(), m_cursor.position());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::insertBeforeCursor(const QString &text)
|
||||||
|
{
|
||||||
|
m_cursor.insertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::insertAfterCursor(const QString &text)
|
||||||
|
{
|
||||||
|
m_cursor.insertText(text);
|
||||||
|
m_cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, text.length());
|
||||||
|
emit cursorPositionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeCompletion::moveCursor(CodeCompletion::MoveOperation moveOperation, int count)
|
||||||
|
{
|
||||||
|
switch (moveOperation) {
|
||||||
|
case MoveOperationPreviousLine:
|
||||||
|
m_cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor, count);
|
||||||
|
emit cursorPositionChanged();
|
||||||
|
return;
|
||||||
|
case MoveOperationNextLine:
|
||||||
|
m_cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, count);
|
||||||
|
emit cursorPositionChanged();
|
||||||
|
return;
|
||||||
|
case MoveOperationPreviousWord: {
|
||||||
|
// We're not using the cursors next/previos word because we want camelCase word fragments
|
||||||
|
QTextCursor tmp = m_document->textDocument()->find(QRegExp("[A-Z\\.:\"'\\(\\)\\[\\]^ ]"), m_cursor.position() - 1, QTextDocument::FindBackward);
|
||||||
|
qWarning() << "found at" << tmp.position() << "starting at" << m_cursor.position();
|
||||||
|
m_cursor.setPosition(tmp.position());
|
||||||
|
emit cursorPositionChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case MoveOperationNextWord: {
|
||||||
|
// We're not using the cursors next/previos word because we want camelCase word fragments
|
||||||
|
QTextCursor tmp = m_document->textDocument()->find(QRegExp("[A-Z\\.:\"'\\(\\)\\[\\]$ ]"), m_cursor.position() + 1);
|
||||||
|
m_cursor.setPosition(tmp.position() - 1);
|
||||||
|
emit cursorPositionChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
libnymea-app-core/scripting/codecompletion.h
Normal file
109
libnymea-app-core/scripting/codecompletion.h
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
#ifndef CODECOMPLETION_H
|
||||||
|
#define CODECOMPLETION_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQuickTextDocument>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
|
#include "completionmodel.h"
|
||||||
|
|
||||||
|
class Engine;
|
||||||
|
|
||||||
|
class CodeCompletion: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(Engine* engine READ engine WRITE setEngine NOTIFY engineChanged)
|
||||||
|
Q_PROPERTY(QQuickTextDocument *document READ document WRITE setDocument NOTIFY documentChanged)
|
||||||
|
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
||||||
|
Q_PROPERTY(CompletionProxyModel* model READ model CONSTANT)
|
||||||
|
Q_PROPERTY(QString currentWord READ currentWord NOTIFY currentWordChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum MoveOperation {
|
||||||
|
MoveOperationPreviousLine,
|
||||||
|
MoveOperationNextLine,
|
||||||
|
MoveOperationPreviousWord,
|
||||||
|
MoveOperationNextWord,
|
||||||
|
};
|
||||||
|
Q_ENUM(MoveOperation)
|
||||||
|
|
||||||
|
CodeCompletion(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
Engine* engine() const;
|
||||||
|
void setEngine(Engine *engine);
|
||||||
|
|
||||||
|
QQuickTextDocument* document() const;
|
||||||
|
void setDocument(QQuickTextDocument *document);
|
||||||
|
|
||||||
|
int cursorPosition() const;
|
||||||
|
void setCursorPosition(int position);
|
||||||
|
|
||||||
|
QString currentWord() const;
|
||||||
|
|
||||||
|
CompletionProxyModel* model() const;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void update();
|
||||||
|
|
||||||
|
void complete(int index);
|
||||||
|
void newLine();
|
||||||
|
void indent(int from, int to);
|
||||||
|
void unindent(int from, int to);
|
||||||
|
void closeBlock();
|
||||||
|
void insertBeforeCursor(const QString &text);
|
||||||
|
void insertAfterCursor(const QString &text);
|
||||||
|
|
||||||
|
void moveCursor(MoveOperation moveOperation, int count = 1);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void engineChanged();
|
||||||
|
void documentChanged();
|
||||||
|
void cursorPositionChanged();
|
||||||
|
void currentWordChanged();
|
||||||
|
void hint();
|
||||||
|
|
||||||
|
private:
|
||||||
|
class BlockInfo {
|
||||||
|
public:
|
||||||
|
bool valid = false;
|
||||||
|
QString name;
|
||||||
|
QHash<QString, QString> properties;
|
||||||
|
int start = -1;
|
||||||
|
int end = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ClassInfo {
|
||||||
|
public:
|
||||||
|
ClassInfo(const QString &name = QString(), const QStringList &properties = QStringList(), const QStringList &methods = QStringList(), const QStringList &events = QStringList()):
|
||||||
|
name(name), properties(properties), methods(methods), events(events) {}
|
||||||
|
QString name;
|
||||||
|
QStringList properties;
|
||||||
|
QStringList methods;
|
||||||
|
QStringList events;
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockInfo getBlockInfo(int postition) const;
|
||||||
|
QList<CompletionModel::Entry> getIds() const;
|
||||||
|
QHash<QString, QString> getIdTypes() const;
|
||||||
|
|
||||||
|
int openingBlocksBefore(int position) const;
|
||||||
|
int closingBlocksAfter(int position) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Engine *m_engine = nullptr;
|
||||||
|
QQuickTextDocument* m_document = nullptr;
|
||||||
|
CompletionModel *m_model = nullptr;
|
||||||
|
CompletionProxyModel *m_proxy = nullptr;
|
||||||
|
|
||||||
|
QTextCursor m_cursor;
|
||||||
|
|
||||||
|
QHash<QString, ClassInfo> m_classes;
|
||||||
|
QHash<QString, ClassInfo> m_attachedClasses;
|
||||||
|
QHash<QString, ClassInfo> m_jsClasses;
|
||||||
|
QHash<QString, QString> m_genericSyntax;
|
||||||
|
QHash<QString, QString> m_genericJsSyntax;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CODECOMPLETION_H
|
||||||
121
libnymea-app-core/scripting/completionmodel.cpp
Normal file
121
libnymea-app-core/scripting/completionmodel.cpp
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#include "completionmodel.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
CompletionModel::CompletionModel(QObject *parent): QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int CompletionModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent)
|
||||||
|
return m_list.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> CompletionModel::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> roles;
|
||||||
|
roles.insert(Qt::UserRole, "text");
|
||||||
|
roles.insert(Qt::DisplayRole, "displayText");
|
||||||
|
roles.insert(Qt::DecorationRole, "decoration");
|
||||||
|
roles.insert(Qt::DecorationPropertyRole, "decorationProperty");
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant CompletionModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case Qt::UserRole:
|
||||||
|
return m_list.at(index.row()).text;
|
||||||
|
case Qt::DisplayRole:
|
||||||
|
return m_list.at(index.row()).displayText;
|
||||||
|
case Qt::DecorationRole:
|
||||||
|
return m_list.at(index.row()).decoration;
|
||||||
|
case Qt::DecorationPropertyRole:
|
||||||
|
return m_list.at(index.row()).decorationProperty;
|
||||||
|
}
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionModel::update(const QList<CompletionModel::Entry> &entries)
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
m_list = entries;
|
||||||
|
endResetModel();
|
||||||
|
emit countChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletionModel::Entry CompletionModel::get(int index)
|
||||||
|
{
|
||||||
|
return m_list.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
//************************************************
|
||||||
|
// CompletionProxyModel
|
||||||
|
//************************************************
|
||||||
|
|
||||||
|
CompletionProxyModel::CompletionProxyModel(CompletionModel *model, QObject *parent):
|
||||||
|
QSortFilterProxyModel(parent),
|
||||||
|
m_model(model)
|
||||||
|
{
|
||||||
|
setSourceModel(m_model);
|
||||||
|
connect(m_model, &CompletionModel::countChanged, this, &CompletionProxyModel::countChanged);
|
||||||
|
setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||||
|
sort(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletionModel::Entry CompletionProxyModel::get(int index)
|
||||||
|
{
|
||||||
|
return m_model->get(mapToSource(this->index(index, 0)).row());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CompletionProxyModel::filter() const
|
||||||
|
{
|
||||||
|
return m_filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompletionProxyModel::setFilter(const QString &filter, bool caseSensitive)
|
||||||
|
{
|
||||||
|
if (m_filter != filter || m_filterCaseSensitive != caseSensitive) {
|
||||||
|
m_filter = filter;
|
||||||
|
m_filterCaseSensitive = caseSensitive;
|
||||||
|
emit filterChanged();
|
||||||
|
invalidateFilter();
|
||||||
|
emit countChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CompletionProxyModel::filterAcceptsRow(int source_row, const QModelIndex &) const
|
||||||
|
{
|
||||||
|
if (!m_filter.isEmpty()) {
|
||||||
|
CompletionModel::Entry entry = m_model->get(source_row);
|
||||||
|
if (m_filterCaseSensitive) {
|
||||||
|
if (!entry.displayText.startsWith(m_filter) && !entry.text.startsWith(m_filter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!entry.displayText.toLower().startsWith(m_filter.toLower()) && !entry.text.toLower().startsWith(m_filter.toLower())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CompletionProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
|
||||||
|
{
|
||||||
|
CompletionModel::Entry left = m_model->get(source_left.row());
|
||||||
|
CompletionModel::Entry right = m_model->get(source_right.row());
|
||||||
|
|
||||||
|
static QStringList ordering = {"property", "method", "event", "type", "keyword" };
|
||||||
|
|
||||||
|
int leftOrder = ordering.indexOf(left.decoration);
|
||||||
|
int rightOrder = ordering.indexOf(right.decoration);
|
||||||
|
|
||||||
|
if (leftOrder != rightOrder) {
|
||||||
|
return leftOrder < rightOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.displayText < right.displayText;
|
||||||
|
}
|
||||||
69
libnymea-app-core/scripting/completionmodel.h
Normal file
69
libnymea-app-core/scripting/completionmodel.h
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#ifndef COMPLETIONMODEL_H
|
||||||
|
#define COMPLETIONMODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QSortFilterProxyModel>
|
||||||
|
|
||||||
|
class CompletionModel: public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||||
|
public:
|
||||||
|
class Entry {
|
||||||
|
public:
|
||||||
|
Entry(const QString &text, const QString &displayText, const QString &decoration, const QString &decorationProperty = QString(), const QString &trailingText = QString())
|
||||||
|
: text(text), displayText(displayText), decoration(decoration), decorationProperty(decorationProperty), trailingText(trailingText) {}
|
||||||
|
Entry(const QString &text): text(text), displayText(text) {}
|
||||||
|
QString text;
|
||||||
|
QString displayText;
|
||||||
|
QString decoration;
|
||||||
|
QString decorationProperty;
|
||||||
|
QString trailingText;
|
||||||
|
};
|
||||||
|
|
||||||
|
CompletionModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
void update(const QList<Entry> &entries);
|
||||||
|
|
||||||
|
Entry get(int index);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void countChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<Entry> m_list;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CompletionProxyModel: public QSortFilterProxyModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||||
|
Q_PROPERTY(QString filter READ filter NOTIFY filterChanged)
|
||||||
|
public:
|
||||||
|
CompletionProxyModel(CompletionModel *model, QObject *parent = nullptr);
|
||||||
|
CompletionModel::Entry get(int index);
|
||||||
|
|
||||||
|
QString filter() const;
|
||||||
|
void setFilter(const QString &filter, bool caseSensitive = true);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool filterAcceptsRow(int source_row, const QModelIndex &/*source_parent*/) const override;
|
||||||
|
|
||||||
|
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void countChanged();
|
||||||
|
void filterChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
CompletionModel *m_model = nullptr;
|
||||||
|
QString m_filter;
|
||||||
|
bool m_filterCaseSensitive = true;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COMPLETIONMODEL_H
|
||||||
148
libnymea-app-core/scriptmanager.cpp
Normal file
148
libnymea-app-core/scriptmanager.cpp
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
#include "scriptmanager.h"
|
||||||
|
|
||||||
|
#include "types/script.h"
|
||||||
|
#include "types/scripts.h"
|
||||||
|
|
||||||
|
ScriptManager::ScriptManager(JsonRpcClient *jsonClient, QObject *parent):
|
||||||
|
JsonHandler(parent),
|
||||||
|
m_client(jsonClient)
|
||||||
|
{
|
||||||
|
m_scripts = new Scripts(this);
|
||||||
|
|
||||||
|
m_client->registerNotificationHandler(this, "onNotificationReceived");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::init()
|
||||||
|
{
|
||||||
|
m_scripts->clear();
|
||||||
|
m_client->sendCommand("Scripts.GetScripts", QVariantMap(), this, "onScriptsFetched");
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ScriptManager::nameSpace() const
|
||||||
|
{
|
||||||
|
return "Scripts";
|
||||||
|
}
|
||||||
|
|
||||||
|
Scripts *ScriptManager::scripts() const
|
||||||
|
{
|
||||||
|
return m_scripts;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScriptManager::addScript(const QString &name, const QString &content)
|
||||||
|
{
|
||||||
|
QVariantMap params;
|
||||||
|
params.insert("name", name);
|
||||||
|
params.insert("content", content);
|
||||||
|
return m_client->sendCommand("Scripts.AddScript", params, this, "onScriptAdded");
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScriptManager::renameScript(const QUuid &id, const QString &name)
|
||||||
|
{
|
||||||
|
QVariantMap params;
|
||||||
|
params.insert("id", id);
|
||||||
|
params.insert("name", name);
|
||||||
|
return m_client->sendCommand("Scripts.EditScript", params, this, "onScriptRenamed");
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScriptManager::editScript(const QUuid &id, const QString &content)
|
||||||
|
{
|
||||||
|
QVariantMap params;
|
||||||
|
params.insert("id", id);
|
||||||
|
params.insert("content", content);
|
||||||
|
return m_client->sendCommand("Scripts.EditScript", params, this, "onScriptEdited");
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScriptManager::removeScript(const QUuid &id)
|
||||||
|
{
|
||||||
|
QVariantMap params;
|
||||||
|
params.insert("id", id);
|
||||||
|
return m_client->sendCommand("Scripts.RemoveScript", params, this, "onScriptRemoved");
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScriptManager::fetchScript(const QUuid &id)
|
||||||
|
{
|
||||||
|
QVariantMap params;
|
||||||
|
params.insert("id", id);
|
||||||
|
return m_client->sendCommand("Scripts.GetScriptContent", params, this, "onScriptFetched");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::onScriptsFetched(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
foreach (const QVariant &variant, params.value("params").toMap().value("scripts").toList()) {
|
||||||
|
QUuid id = variant.toMap().value("id").toUuid();
|
||||||
|
Script *script = new Script(id);
|
||||||
|
script->setName(variant.toMap().value("name").toString());
|
||||||
|
m_scripts->addScript(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::onScriptFetched(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
emit fetchScriptReply(params.value("id").toInt(),
|
||||||
|
params.value("params").toMap().value("scriptError").toString(),
|
||||||
|
params.value("params").toMap().value("content").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::onScriptAdded(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
emit addScriptReply(params.value("id").toInt(),
|
||||||
|
params.value("params").toMap().value("scriptError").toString(),
|
||||||
|
params.value("params").toMap().value("script").toMap().value("id").toUuid(),
|
||||||
|
params.value("params").toMap().value("errors").toStringList());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::onScriptEdited(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
emit editScriptReply(params.value("id").toInt(),
|
||||||
|
params.value("params").toMap().value("scriptError").toString(),
|
||||||
|
params.value("params").toMap().value("errors").toStringList());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::onScriptRenamed(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
emit renameScriptReply(params.value("id").toInt(), params.value("params").toMap().value("scriptError").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::onScriptRemoved(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
emit removeScriptReply(params.value("id").toInt(), params.value("params").toMap().value("scriptError").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptManager::onNotificationReceived(const QVariantMap ¶ms)
|
||||||
|
{
|
||||||
|
qDebug() << "noticication" << params.value("notification").toString();
|
||||||
|
if (params.value("notification").toString() == "Scripts.ScriptLogMessage") {
|
||||||
|
emit scriptMessage(params.value("params").toMap().value("scriptId").toUuid(),
|
||||||
|
params.value("params").toMap().value("type").toString(),
|
||||||
|
params.value("params").toMap().value("message").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (params.value("notification").toString() == "Scripts.ScriptAdded") {
|
||||||
|
QVariantMap scriptMap = params.value("params").toMap().value("script").toMap();
|
||||||
|
Script *script = new Script(scriptMap.value("id").toUuid());
|
||||||
|
script->setName(scriptMap.value("name").toString());
|
||||||
|
m_scripts->addScript(script);
|
||||||
|
emit addScriptReply(params.value("id").toInt(),
|
||||||
|
params.value("params").toMap().value("scriptError").toString(),
|
||||||
|
params.value("params").toMap().value("scriptId").toUuid(),
|
||||||
|
params.value("params").toMap().value("errors").toStringList());
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (params.value("notification").toString() == "Scripts.ScriptRemoved") {
|
||||||
|
QUuid id = params.value("params").toMap().value("id").toUuid();
|
||||||
|
m_scripts->removeScript(id);
|
||||||
|
emit removeScriptReply(params.value("id").toInt(), params.value("params").toMap().value("scriptError").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (params.value("notification").toString() == "Scripts.ScriptChanged") {
|
||||||
|
QUuid id = params.value("params").toMap().value("scriptId").toUuid();
|
||||||
|
QString name = params.value("params").toMap().value("name").toString();
|
||||||
|
m_scripts->getScript(id)->setName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
qWarning() << "Unhandled notification" << params.value("notification").toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
libnymea-app-core/scriptmanager.h
Normal file
54
libnymea-app-core/scriptmanager.h
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
#ifndef SCRIPTMANAGER_H
|
||||||
|
#define SCRIPTMANAGER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "jsonrpc/jsonrpcclient.h"
|
||||||
|
|
||||||
|
class Scripts;
|
||||||
|
|
||||||
|
class ScriptManager : public JsonHandler
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(Scripts* scripts READ scripts CONSTANT)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ScriptManager(JsonRpcClient* jsonClient, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void init();
|
||||||
|
|
||||||
|
QString nameSpace() const override;
|
||||||
|
|
||||||
|
Scripts *scripts() const;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
int addScript(const QString &name, const QString &content);
|
||||||
|
int renameScript(const QUuid &id, const QString &name);
|
||||||
|
int editScript(const QUuid &id, const QString &content);
|
||||||
|
int removeScript(const QUuid &id);
|
||||||
|
int fetchScript(const QUuid &id);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void addScriptReply(int id, const QString &scriptError, const QUuid &scriptId, const QStringList &errors);
|
||||||
|
void editScriptReply(int id, const QString &scriptError, const QStringList &errors);
|
||||||
|
void renameScriptReply(int id, const QString &scriptError);
|
||||||
|
void removeScriptReply(int id, const QString &scriptError);
|
||||||
|
void fetchScriptReply(int id, const QString &scriptError, const QString &content);
|
||||||
|
|
||||||
|
void scriptMessage(const QUuid &scriptId, const QString &type, const QString &message);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onScriptsFetched(const QVariantMap ¶ms);
|
||||||
|
void onScriptFetched(const QVariantMap ¶ms);
|
||||||
|
void onScriptAdded(const QVariantMap ¶ms);
|
||||||
|
void onScriptEdited(const QVariantMap ¶ms);
|
||||||
|
void onScriptRenamed(const QVariantMap ¶ms);
|
||||||
|
void onScriptRemoved(const QVariantMap ¶ms);
|
||||||
|
|
||||||
|
void onNotificationReceived(const QVariantMap ¶ms);
|
||||||
|
private:
|
||||||
|
JsonRpcClient* m_client = nullptr;
|
||||||
|
Scripts *m_scripts = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SCRIPTMANAGER_H
|
||||||
189
libnymea-app-core/scriptsyntaxhighlighter.cpp
Normal file
189
libnymea-app-core/scriptsyntaxhighlighter.cpp
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
#include "scriptsyntaxhighlighter.h"
|
||||||
|
|
||||||
|
#include "engine.h"
|
||||||
|
#include "devicemanager.h"
|
||||||
|
#include "devices.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QMetaObject>
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
|
#include <QQuickItem>
|
||||||
|
|
||||||
|
class ScriptSyntaxHighlighterPrivate: public QSyntaxHighlighter
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
ScriptSyntaxHighlighterPrivate(QObject *parent);
|
||||||
|
|
||||||
|
void update(bool dark);
|
||||||
|
protected:
|
||||||
|
void highlightBlock(const QString &text) override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void contentChanged(const QString &text);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum BlockState {
|
||||||
|
BlockStateInvalid = -1,
|
||||||
|
BlockStateNone = 0,
|
||||||
|
BlockStateImport = 1,
|
||||||
|
BlockStateAction,
|
||||||
|
BlockstateDeviceId,
|
||||||
|
};
|
||||||
|
struct HighlightingRule
|
||||||
|
{
|
||||||
|
QRegExp pattern;
|
||||||
|
QTextCharFormat format;
|
||||||
|
};
|
||||||
|
QVector<HighlightingRule> highlightingRules;
|
||||||
|
};
|
||||||
|
|
||||||
|
ScriptSyntaxHighlighter::ScriptSyntaxHighlighter(QObject *parent) : QObject(parent)
|
||||||
|
{
|
||||||
|
m_highlighter = new ScriptSyntaxHighlighterPrivate(this);
|
||||||
|
m_highlighter->update(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickTextDocument *ScriptSyntaxHighlighter::document() const
|
||||||
|
{
|
||||||
|
return m_document;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptSyntaxHighlighter::setDocument(QQuickTextDocument *document)
|
||||||
|
{
|
||||||
|
if (m_document != document) {
|
||||||
|
m_document = document;
|
||||||
|
m_highlighter->setDocument(m_document->textDocument());
|
||||||
|
emit documentChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor ScriptSyntaxHighlighter::backgroundColor() const
|
||||||
|
{
|
||||||
|
return m_backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptSyntaxHighlighter::setBackgroundColor(const QColor &backgroundColor)
|
||||||
|
{
|
||||||
|
if (m_backgroundColor != backgroundColor) {
|
||||||
|
m_backgroundColor = backgroundColor;
|
||||||
|
emit backgroundColorChanged();
|
||||||
|
|
||||||
|
double y = 0.2126 * backgroundColor.red() + 0.7152 * backgroundColor.green() + 0.0722 * backgroundColor.blue();
|
||||||
|
m_highlighter->update(y < 128);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ScriptSyntaxHighlighterPrivate::ScriptSyntaxHighlighterPrivate(QObject *parent):
|
||||||
|
QSyntaxHighlighter(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptSyntaxHighlighterPrivate::update(bool dark)
|
||||||
|
{
|
||||||
|
HighlightingRule rule;
|
||||||
|
QTextCharFormat format;
|
||||||
|
|
||||||
|
// ClassNames
|
||||||
|
format.setForeground(dark ? QColor("#55fc49") : QColor("#800080"));
|
||||||
|
rule.pattern = QRegExp("\\b[A-Z][a-zA-Z0-9_]+\\b");
|
||||||
|
rule.format = format;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
// Property bindings
|
||||||
|
format.setForeground(dark ? QColor("#ff5555") : QColor("#800000"));
|
||||||
|
rule.pattern = QRegExp("[a-zA-Z][a-zA-Z0-9_.]+:");
|
||||||
|
rule.format = format;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
// imports
|
||||||
|
format.clearForeground();
|
||||||
|
rule.pattern = QRegExp("import .*$");
|
||||||
|
rule.format = format;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
// keywords
|
||||||
|
QStringList keywordPatterns {
|
||||||
|
"\\bif\\b",
|
||||||
|
"\\belse\\b" ,
|
||||||
|
"\\breturn\\b",
|
||||||
|
"\\bimport\\b",
|
||||||
|
"\\bsignal\\b",
|
||||||
|
"\\bproperty\\b",
|
||||||
|
"\\bfunction\\b",
|
||||||
|
"\\breadonly\\b",
|
||||||
|
"\\balias\\b",
|
||||||
|
"\\bfor\\b",
|
||||||
|
"\\bwhile\\b",
|
||||||
|
"\\bbreak\\b",
|
||||||
|
"\\bswitch\\b",
|
||||||
|
"\\bcase\\b",
|
||||||
|
"\\bdefault\\b",
|
||||||
|
"\\bvar\\b",
|
||||||
|
"\\bnull\\b",
|
||||||
|
"\\bundefined\\b",
|
||||||
|
"\\bstring\\b",
|
||||||
|
"\\bbool\\b",
|
||||||
|
"\\bint\\b",
|
||||||
|
"\\breal\\b",
|
||||||
|
"\\bdate\\b",
|
||||||
|
"\\btrue\\b",
|
||||||
|
"\\bfalse\\b",
|
||||||
|
};
|
||||||
|
format.setForeground(dark ? Qt::yellow : QColor("#80831a"));
|
||||||
|
foreach (const QString &pattern, keywordPatterns) {
|
||||||
|
rule.pattern = QRegExp(pattern);
|
||||||
|
rule.format = format;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String literals
|
||||||
|
format.setForeground(dark ? QColor("#e64ad7") : Qt::darkGreen);
|
||||||
|
rule.format = format;
|
||||||
|
rule.pattern = QRegExp("\".[^\"]*\"");
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
rule.pattern = QRegExp("'.[^']*'");
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
// comments
|
||||||
|
format.setForeground(dark ? Qt::cyan : Qt::darkGray);
|
||||||
|
rule.format = format;
|
||||||
|
rule.pattern = QRegExp("//.*$");
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
rule.pattern = QRegExp("/*.*\\*/");
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptSyntaxHighlighterPrivate::highlightBlock(const QString &text)
|
||||||
|
{
|
||||||
|
// qDebug() << "hightlightBlock called for" << text << previousBlockState() << currentBlock().text();
|
||||||
|
|
||||||
|
foreach(const HighlightingRule &rule, highlightingRules){
|
||||||
|
QRegExp expression(rule.pattern);
|
||||||
|
int index = expression.indexIn(text);
|
||||||
|
while (index >= 0) {
|
||||||
|
int length = expression.matchedLength();
|
||||||
|
if (text.mid(index, length).endsWith(':')) {
|
||||||
|
length--;
|
||||||
|
}
|
||||||
|
setFormat(index, length, rule.format);
|
||||||
|
index = expression.indexIn(text, index + length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (text.trimmed().startsWith("import")) {
|
||||||
|
setCurrentBlockState(BlockStateImport);
|
||||||
|
} else if (text.trimmed().startsWith("Action")) {
|
||||||
|
setCurrentBlockState(BlockStateAction);
|
||||||
|
} else if (text.trimmed().startsWith("deviceId:")) {
|
||||||
|
setCurrentBlockState(BlockstateDeviceId);
|
||||||
|
} else {
|
||||||
|
setCurrentBlockState(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit contentChanged(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#include "scriptsyntaxhighlighter.moc"
|
||||||
37
libnymea-app-core/scriptsyntaxhighlighter.h
Normal file
37
libnymea-app-core/scriptsyntaxhighlighter.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#ifndef SCRIPTSYNTAXHIGHLIGHTER_H
|
||||||
|
#define SCRIPTSYNTAXHIGHLIGHTER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QSyntaxHighlighter>
|
||||||
|
#include <QQuickTextDocument>
|
||||||
|
|
||||||
|
class ScriptSyntaxHighlighterPrivate;
|
||||||
|
class CompletionModel;
|
||||||
|
class CompletionProxyModel;
|
||||||
|
|
||||||
|
class ScriptSyntaxHighlighter : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QQuickTextDocument* document READ document WRITE setDocument NOTIFY documentChanged)
|
||||||
|
Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
|
||||||
|
public:
|
||||||
|
|
||||||
|
explicit ScriptSyntaxHighlighter(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QQuickTextDocument* document() const;
|
||||||
|
void setDocument(QQuickTextDocument *document);
|
||||||
|
|
||||||
|
QColor backgroundColor() const;
|
||||||
|
void setBackgroundColor(const QColor &backgroundColor);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void documentChanged();
|
||||||
|
void backgroundColorChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ScriptSyntaxHighlighterPrivate *m_highlighter = nullptr;
|
||||||
|
QQuickTextDocument* m_document = nullptr;
|
||||||
|
QColor m_backgroundColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SCRIPTSYNTAXHIGHLIGHTER_H
|
||||||
@ -16,6 +16,8 @@ HEADERS += \
|
|||||||
types/packages.h \
|
types/packages.h \
|
||||||
types/repositories.h \
|
types/repositories.h \
|
||||||
types/repository.h \
|
types/repository.h \
|
||||||
|
types/script.h \
|
||||||
|
types/scripts.h \
|
||||||
types/types.h \
|
types/types.h \
|
||||||
types/vendor.h \
|
types/vendor.h \
|
||||||
types/vendors.h \
|
types/vendors.h \
|
||||||
@ -72,6 +74,8 @@ SOURCES += \
|
|||||||
types/packages.cpp \
|
types/packages.cpp \
|
||||||
types/repositories.cpp \
|
types/repositories.cpp \
|
||||||
types/repository.cpp \
|
types/repository.cpp \
|
||||||
|
types/script.cpp \
|
||||||
|
types/scripts.cpp \
|
||||||
types/vendor.cpp \
|
types/vendor.cpp \
|
||||||
types/vendors.cpp \
|
types/vendors.cpp \
|
||||||
types/deviceclass.cpp \
|
types/deviceclass.cpp \
|
||||||
|
|||||||
26
libnymea-common/types/script.cpp
Normal file
26
libnymea-common/types/script.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#include "script.h"
|
||||||
|
|
||||||
|
Script::Script(const QUuid &id, QObject *parent):
|
||||||
|
QObject(parent),
|
||||||
|
m_id(id)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QUuid Script::id() const
|
||||||
|
{
|
||||||
|
return m_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Script::name() const
|
||||||
|
{
|
||||||
|
return m_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Script::setName(const QString &name)
|
||||||
|
{
|
||||||
|
if (m_name != name) {
|
||||||
|
m_name = name;
|
||||||
|
emit nameChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
libnymea-common/types/script.h
Normal file
28
libnymea-common/types/script.h
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifndef SCRIPT_H
|
||||||
|
#define SCRIPT_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
class Script : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QUuid id READ id CONSTANT)
|
||||||
|
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
|
||||||
|
public:
|
||||||
|
explicit Script(const QUuid &id, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QUuid id() const;
|
||||||
|
|
||||||
|
QString name() const;
|
||||||
|
void setName(const QString &name);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void nameChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QUuid m_id;
|
||||||
|
QString m_name;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SCRIPT_H
|
||||||
89
libnymea-common/types/scripts.cpp
Normal file
89
libnymea-common/types/scripts.cpp
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
#include "scripts.h"
|
||||||
|
|
||||||
|
#include "script.h"
|
||||||
|
|
||||||
|
Scripts::Scripts(QObject *parent) : QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int Scripts::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent)
|
||||||
|
return m_list.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant Scripts::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case RoleId:
|
||||||
|
return m_list.at(index.row())->id();
|
||||||
|
case RoleName:
|
||||||
|
return m_list.at(index.row())->name();
|
||||||
|
}
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> Scripts::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> roles;
|
||||||
|
roles.insert(RoleId, "id");
|
||||||
|
roles.insert(RoleName, "name");
|
||||||
|
return roles;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scripts::clear()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
qDeleteAll(m_list);
|
||||||
|
m_list.clear();
|
||||||
|
endResetModel();
|
||||||
|
emit countChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scripts::addScript(Script *script)
|
||||||
|
{
|
||||||
|
script->setParent(this);
|
||||||
|
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
|
||||||
|
m_list.append(script);
|
||||||
|
endInsertRows();
|
||||||
|
emit countChanged();
|
||||||
|
|
||||||
|
connect(script, &Script::nameChanged, this, [this, script](){
|
||||||
|
int idx = m_list.indexOf(script);
|
||||||
|
if (idx < 0) return;
|
||||||
|
emit dataChanged(index(idx), index(idx), {RoleName});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scripts::removeScript(const QUuid &id)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_list.count(); i++) {
|
||||||
|
if (m_list.at(i)->id() == id) {
|
||||||
|
beginRemoveRows(QModelIndex(), i, i);
|
||||||
|
m_list.takeAt(i)->deleteLater();
|
||||||
|
endRemoveRows();
|
||||||
|
emit countChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Script* Scripts::get(int index) const
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_list.count()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return m_list.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Script *Scripts::getScript(const QUuid &scriptId)
|
||||||
|
{
|
||||||
|
foreach (Script *script, m_list) {
|
||||||
|
if (script->id() == scriptId) {
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
40
libnymea-common/types/scripts.h
Normal file
40
libnymea-common/types/scripts.h
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#ifndef SCRIPTS_H
|
||||||
|
#define SCRIPTS_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
class Script;
|
||||||
|
|
||||||
|
class Scripts : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||||
|
public:
|
||||||
|
enum Roles {
|
||||||
|
RoleId,
|
||||||
|
RoleName
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit Scripts(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
void addScript(Script *script);
|
||||||
|
void removeScript(const QUuid &id);
|
||||||
|
|
||||||
|
Q_INVOKABLE Script *get(int index) const;
|
||||||
|
Q_INVOKABLE Script *getScript(const QUuid &scriptId);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void countChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<Script*> m_list;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SCRIPTS_H
|
||||||
@ -213,5 +213,8 @@
|
|||||||
<file>ui/images/browser/MediaBrowserIconSoundCloud.svg</file>
|
<file>ui/images/browser/MediaBrowserIconSoundCloud.svg</file>
|
||||||
<file>ui/images/browser/MediaBrowserIconDeezer.svg</file>
|
<file>ui/images/browser/MediaBrowserIconDeezer.svg</file>
|
||||||
<file>ui/images/view-grid-symbolic.svg</file>
|
<file>ui/images/view-grid-symbolic.svg</file>
|
||||||
|
<file>ui/images/script.svg</file>
|
||||||
|
<file>ui/images/save.svg</file>
|
||||||
|
<file>ui/images/edit-clear.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@ -203,5 +203,10 @@
|
|||||||
<file>ui/delegates/ThingTile.qml</file>
|
<file>ui/delegates/ThingTile.qml</file>
|
||||||
<file>ui/components/TimePicker.qml</file>
|
<file>ui/components/TimePicker.qml</file>
|
||||||
<file>ui/components/DatePicker.qml</file>
|
<file>ui/components/DatePicker.qml</file>
|
||||||
|
<file>ui/magic/ScriptEditor.qml</file>
|
||||||
|
<file>ui/magic/ScriptsPage.qml</file>
|
||||||
|
<file>ui/magic/scripting/LineNumbers.qml</file>
|
||||||
|
<file>ui/magic/scripting/CompletionBox.qml</file>
|
||||||
|
<file>ui/magic/scripting/EditorPane.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
@ -10,6 +10,14 @@ Page {
|
|||||||
text: qsTr("Magic")
|
text: qsTr("Magic")
|
||||||
onBackPressed: pageStack.pop()
|
onBackPressed: pageStack.pop()
|
||||||
|
|
||||||
|
HeaderButton {
|
||||||
|
imageSource: Qt.resolvedUrl("images/script.svg")
|
||||||
|
visible: engine.jsonRpcClient.ensureServerVersion("4.1")
|
||||||
|
onClicked: {
|
||||||
|
pageStack.push("magic/ScriptsPage.qml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HeaderButton {
|
HeaderButton {
|
||||||
imageSource: Qt.resolvedUrl("images/add.svg")
|
imageSource: Qt.resolvedUrl("images/add.svg")
|
||||||
onClicked: {
|
onClicked: {
|
||||||
@ -21,7 +29,7 @@ Page {
|
|||||||
RuleTemplatesFilterModel {
|
RuleTemplatesFilterModel {
|
||||||
id: ruleTemplatesModel
|
id: ruleTemplatesModel
|
||||||
ruleTemplates: RuleTemplates {}
|
ruleTemplates: RuleTemplates {}
|
||||||
readonly property var deviceClass: device ? engine.deviceManager.deviceClasses.getDeviceClass(root.device.deviceClassId) : null
|
readonly property var deviceClass: root.device ? engine.deviceManager.deviceClasses.getDeviceClass(root.device.deviceClassId) : null
|
||||||
filterByDevices: DevicesProxy { engine: _engine }
|
filterByDevices: DevicesProxy { engine: _engine }
|
||||||
filterInterfaceNames: deviceClass ? deviceClass.interfaces : []
|
filterInterfaceNames: deviceClass ? deviceClass.interfaces : []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ ApplicationWindow {
|
|||||||
property int largeFont: 20
|
property int largeFont: 20
|
||||||
property int iconSize: 30
|
property int iconSize: 30
|
||||||
property int delegateHeight: 60
|
property int delegateHeight: 60
|
||||||
|
property color backgroundColor: Material.background
|
||||||
|
|
||||||
readonly property bool landscape: app.width > app.height
|
readonly property bool landscape: app.width > app.height
|
||||||
|
|
||||||
|
|||||||
@ -58,6 +58,7 @@ Item {
|
|||||||
font.pixelSize: app.mediumFont
|
font.pixelSize: app.mediumFont
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
text: root.text
|
text: root.text
|
||||||
|
visible: text.length > 0
|
||||||
color: app.headerForegroundColor
|
color: app.headerForegroundColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
nymea-app/ui/images/edit-clear.svg
Normal file
182
nymea-app/ui/images/edit-clear.svg
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="96"
|
||||||
|
height="96"
|
||||||
|
id="svg4874"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91+devel r"
|
||||||
|
viewBox="0 0 96 96.000001"
|
||||||
|
sodipodi:docname="edit-clear.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4876" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="10.976561"
|
||||||
|
inkscape:cx="19.159003"
|
||||||
|
inkscape:cy="42.886827"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="g4780"
|
||||||
|
showgrid="true"
|
||||||
|
showborder="true"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:snap-bbox-midpoints="true"
|
||||||
|
inkscape:object-paths="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:object-nodes="true"
|
||||||
|
inkscape:snap-smooth-nodes="true"
|
||||||
|
inkscape:snap-midpoints="true"
|
||||||
|
inkscape:snap-object-midpoints="true"
|
||||||
|
inkscape:snap-center="true"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true"
|
||||||
|
inkscape:snap-global="true">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid5451"
|
||||||
|
empspacing="8" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="8,-8.0000001"
|
||||||
|
id="guide4063" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="4,-8.0000001"
|
||||||
|
id="guide4065" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-8,88.000001"
|
||||||
|
id="guide4067" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-8,92.000001"
|
||||||
|
id="guide4069" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="104,4"
|
||||||
|
id="guide4071" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-5,8.0000001"
|
||||||
|
id="guide4073" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="92,-8.0000001"
|
||||||
|
id="guide4075" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="88,-8.0000001"
|
||||||
|
id="guide4077" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-8,84.000001"
|
||||||
|
id="guide4074" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="12,-8.0000001"
|
||||||
|
id="guide4076" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-5,12"
|
||||||
|
id="guide4078" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="84,-9.0000001"
|
||||||
|
id="guide4080" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="48,-8.0000001"
|
||||||
|
orientation="1,0"
|
||||||
|
id="guide4170" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="-8,48"
|
||||||
|
orientation="0,1"
|
||||||
|
id="guide4172" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata4879">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(67.857146,-78.50504)">
|
||||||
|
<g
|
||||||
|
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
|
||||||
|
id="g4845"
|
||||||
|
style="display:inline">
|
||||||
|
<g
|
||||||
|
inkscape:export-ydpi="90"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-filename="next01.png"
|
||||||
|
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
|
||||||
|
id="g4778"
|
||||||
|
inkscape:label="Layer 1">
|
||||||
|
<g
|
||||||
|
transform="matrix(-1,0,0,1,575.99999,611)"
|
||||||
|
id="g4780"
|
||||||
|
style="display:inline">
|
||||||
|
<rect
|
||||||
|
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
|
||||||
|
id="rect4782"
|
||||||
|
width="96.037987"
|
||||||
|
height="96"
|
||||||
|
x="-438.00244"
|
||||||
|
y="345.36221"
|
||||||
|
transform="scale(-1,1)" />
|
||||||
|
<path
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#808080;fill-opacity:1;stroke:none"
|
||||||
|
d="m 364.0904,368.96573 c -0.0215,-0.0161 -0.0354,-0.0404 -0.0567,-0.0566 -0.0253,-0.0201 -0.057,-0.0305 -0.0821,-0.0508 z"
|
||||||
|
id="path4157" />
|
||||||
|
<path
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#808080;fill-opacity:1;stroke:none"
|
||||||
|
d="m 364.07673,417.80167 -0.13873,0.10742 c 0.0251,-0.0203 0.0569,-0.0307 0.0821,-0.0508 0.0214,-0.0162 0.0353,-0.0405 0.0567,-0.0566 z"
|
||||||
|
id="path4344" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 432,393.36133 c 0,23.17236 -18.83538,42 -42.01562,42 -23.18025,0 -42.01563,-18.82764 -42.01563,-42 0,-23.17236 18.83538,-42 42.01563,-42 23.18024,0 42.01562,18.82764 42.01562,42 z m -4.00195,0 c 0,-21.00932 -16.99476,-37.99805 -38.01367,-37.99805 -21.01892,0 -38.01563,16.98873 -38.01563,37.99805 0,21.00931 16.99671,37.99804 38.01563,37.99805 21.01891,0 38.01367,-16.98874 38.01367,-37.99805 z"
|
||||||
|
id="path4116"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 406.57617,373.94727 -36.01367,36 2.82812,2.83007 36.01368,-36 -2.82813,-2.83007 z"
|
||||||
|
id="path4305"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||||
|
d="m 373.39062,373.94727 -2.82812,2.83007 36.01367,36 2.82813,-2.83007 -36.01368,-36 z"
|
||||||
|
id="path4307"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 9.8 KiB |
170
nymea-app/ui/images/save.svg
Normal file
170
nymea-app/ui/images/save.svg
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="96"
|
||||||
|
height="96"
|
||||||
|
id="svg4874"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91+devel r"
|
||||||
|
viewBox="0 0 96 96.000001"
|
||||||
|
sodipodi:docname="save.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4876" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="11.258999"
|
||||||
|
inkscape:cx="40.139054"
|
||||||
|
inkscape:cy="22.294719"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="g4780"
|
||||||
|
showgrid="true"
|
||||||
|
showborder="true"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:snap-bbox-midpoints="true"
|
||||||
|
inkscape:object-paths="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:object-nodes="true"
|
||||||
|
inkscape:snap-smooth-nodes="true"
|
||||||
|
inkscape:snap-midpoints="true"
|
||||||
|
inkscape:snap-object-midpoints="true"
|
||||||
|
inkscape:snap-center="true"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid5451"
|
||||||
|
empspacing="8" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="8,-8.0000001"
|
||||||
|
id="guide4063" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="4,-8.0000001"
|
||||||
|
id="guide4065" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-8,88.000001"
|
||||||
|
id="guide4067" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-8,92.000001"
|
||||||
|
id="guide4069" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="104,4"
|
||||||
|
id="guide4071" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-5,8.0000001"
|
||||||
|
id="guide4073" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="92,-8.0000001"
|
||||||
|
id="guide4075" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="88,-8.0000001"
|
||||||
|
id="guide4077" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-8,84.000001"
|
||||||
|
id="guide4074" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="12,-8.0000001"
|
||||||
|
id="guide4076" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="-5,12"
|
||||||
|
id="guide4078" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="84,-9.0000001"
|
||||||
|
id="guide4080" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="48,-8.0000001"
|
||||||
|
orientation="1,0"
|
||||||
|
id="guide4170" />
|
||||||
|
<sodipodi:guide
|
||||||
|
position="-8,48"
|
||||||
|
orientation="0,1"
|
||||||
|
id="guide4172" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata4879">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(67.857146,-78.50504)">
|
||||||
|
<g
|
||||||
|
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
|
||||||
|
id="g4845"
|
||||||
|
style="display:inline">
|
||||||
|
<g
|
||||||
|
inkscape:export-ydpi="90"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-filename="next01.png"
|
||||||
|
transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
|
||||||
|
id="g4778"
|
||||||
|
inkscape:label="Layer 1">
|
||||||
|
<g
|
||||||
|
transform="matrix(-1,0,0,1,575.99999,611)"
|
||||||
|
id="g4780"
|
||||||
|
style="display:inline">
|
||||||
|
<path
|
||||||
|
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.99999952;marker:none;enable-background:accumulate"
|
||||||
|
d="m 436.00166,403.36222 -32.01266,0 0,13.625 c 0,0 -24.94685,-11.15391 -42.01662,-23.62305 17.06977,-12.46913 42.01662,-23.62695 42.01662,-23.62695 l 0,13.625 32.01266,0 z m -4.00158,-4 0,-12 -32.01266,0 0,-11.21289 c -5.98966,2.82047 -18.03813,8.81417 -30.68011,17.21484 12.64206,8.40027 24.69078,14.39149 30.68011,17.21094 l 0,-11.21289 z"
|
||||||
|
id="path4179"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccccccccccccccc" />
|
||||||
|
<rect
|
||||||
|
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
|
||||||
|
id="rect4782"
|
||||||
|
width="96.037987"
|
||||||
|
height="96"
|
||||||
|
x="-438.00244"
|
||||||
|
y="345.36221"
|
||||||
|
transform="scale(-1,1)" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:8.00158233, 8.00158233;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
|
||||||
|
d="m 355.97,425.36222 -12.00475,0 0,-64 12.00475,0 z m -4.00158,-52 0,-8 -4.00159,0 0,8 z"
|
||||||
|
id="rect4181"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="cccccccccc" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
157
nymea-app/ui/images/script.svg
Normal file
157
nymea-app/ui/images/script.svg
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="90"
|
||||||
|
height="90"
|
||||||
|
id="svg4874"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.48+devel r"
|
||||||
|
viewBox="0 0 90 90.000001"
|
||||||
|
sodipodi:docname="text-css-symbolic.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4876" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="5.0931703"
|
||||||
|
inkscape:cx="25.161142"
|
||||||
|
inkscape:cy="58.450824"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="g5283"
|
||||||
|
showgrid="true"
|
||||||
|
showborder="true"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:snap-bbox-midpoints="true"
|
||||||
|
inkscape:object-paths="true"
|
||||||
|
inkscape:snap-intersection-paths="true"
|
||||||
|
inkscape:object-nodes="true"
|
||||||
|
inkscape:snap-smooth-nodes="true"
|
||||||
|
inkscape:snap-midpoints="true"
|
||||||
|
inkscape:snap-object-midpoints="true"
|
||||||
|
inkscape:snap-center="true"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:guide-bbox="true">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid5451"
|
||||||
|
empspacing="6" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="6,77"
|
||||||
|
id="guide4063" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="3,78"
|
||||||
|
id="guide4065" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="55,84"
|
||||||
|
id="guide4067" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="53,87"
|
||||||
|
id="guide4069" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="20,3"
|
||||||
|
id="guide4071" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="20,6"
|
||||||
|
id="guide4073" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="87,7"
|
||||||
|
id="guide4075" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="84,7"
|
||||||
|
id="guide4077" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="58,81"
|
||||||
|
id="guide4074" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="9,74"
|
||||||
|
id="guide4076" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="0,1"
|
||||||
|
position="21,9"
|
||||||
|
id="guide4078" />
|
||||||
|
<sodipodi:guide
|
||||||
|
orientation="1,0"
|
||||||
|
position="81,4"
|
||||||
|
id="guide4080" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata4879">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(67.857146,-84.50504)">
|
||||||
|
<g
|
||||||
|
transform="matrix(0,-1,-1,0,373.50506,516.50504)"
|
||||||
|
id="g4845"
|
||||||
|
style="display:inline">
|
||||||
|
<g
|
||||||
|
id="g5283"
|
||||||
|
transform="matrix(0,-1,-1,0,-293.63782,2219.3622)">
|
||||||
|
<rect
|
||||||
|
y="-725.63782"
|
||||||
|
x="1778"
|
||||||
|
height="90"
|
||||||
|
width="90"
|
||||||
|
id="rect5285"
|
||||||
|
style="fill:none;stroke:none" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
|
||||||
|
d="m 9,3 0,84 51,0 3,0 18,-18 0,-3 0,-63 z m 6,6 60,0 0,57 -15,0 0,15 -45,0 z"
|
||||||
|
transform="translate(1778,-725.63782)"
|
||||||
|
id="path5289"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="ccccccccccccccc" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path4154"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:medium;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Medium';text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#808080;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 1807.0574,-682.67712 q 1.9358,0 2.8196,-1.21488 0.9258,-1.25826 0.9258,-2.95041 l 0,-6.37809 q 0,-2.03926 0.3787,-3.60124 0.3788,-1.60537 1.3467,-2.64669 0.9679,-1.08471 2.6934,-1.60537 1.7254,-0.56405 4.4187,-0.56405 l 0.2947,0 0,4.16528 q -1.2205,0 -2.0622,0.17356 -0.8417,0.17355 -1.3466,0.65082 -0.505,0.43389 -0.7575,1.21488 -0.2105,0.7376 -0.2105,1.90909 l 0,6.20454 q 0,2.60331 -0.6733,4.16529 -0.6312,1.51859 -2.1884,2.51653 1.5572,0.99793 2.1884,2.55991 0.6733,1.56198 0.6733,4.12189 l 0,6.24793 q 0,1.17149 0.2105,1.9091 0.2525,0.7376 0.7575,1.17148 0.5049,0.47727 1.3466,0.65083 0.8417,0.17355 2.0622,0.17355 l 0,4.16528 q -2.8197,0 -4.5872,-0.52065 -1.7675,-0.52066 -2.7775,-1.60537 -1.01,-1.04133 -1.3888,-2.60331 -0.3787,-1.56198 -0.3787,-3.64462 l 0,-6.42149 q 0,-1.64875 -0.9258,-2.86363 -0.8838,-1.25827 -2.8196,-1.25827 l 0,-4.12189 z" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path4156"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:medium;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Medium';text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#808080;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 1839,-678.55523 q -1.9359,0 -2.8617,1.25827 -0.8838,1.21488 -0.8838,2.86363 l 0,6.42149 q 0,2.08264 -0.3788,3.64462 -0.3786,1.56198 -1.3887,2.60331 -1.01,1.08471 -2.7775,1.60537 -1.7675,0.52065 -4.5871,0.52065 l 0,-4.16528 q 2.4409,0 3.4088,-0.82438 0.9679,-0.78099 0.9679,-3.08058 l 0,-6.24793 q 0,-2.55991 0.6313,-4.12189 0.6733,-1.56198 2.2304,-2.55991 -1.5571,-0.99794 -2.2304,-2.51653 -0.6313,-1.56198 -0.6313,-4.16529 l 0,-6.20454 q 0,-1.17149 -0.2525,-1.90909 -0.2104,-0.78099 -0.7154,-1.21488 -0.5051,-0.47727 -1.3467,-0.65082 -0.8417,-0.17356 -2.0621,-0.17356 l 0,-4.16528 0.2946,0 q 2.6933,0 4.4188,0.56405 1.7254,0.52066 2.6933,1.60537 0.968,1.04132 1.3466,2.64669 0.3788,1.56198 0.3788,3.60124 l 0,6.37809 q 0,1.69215 0.8838,2.95041 0.9258,1.21488 2.8617,1.21488 l 0,4.12189 z" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
424
nymea-app/ui/magic/ScriptEditor.qml
Normal file
424
nymea-app/ui/magic/ScriptEditor.qml
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
import QtQuick 2.4
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import Nymea 1.0
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import QtQuick.Controls.Material 2.1
|
||||||
|
import Qt.labs.settings 1.0
|
||||||
|
import "../components"
|
||||||
|
import "scripting"
|
||||||
|
|
||||||
|
Page {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias scriptId: d.scriptId
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (scriptId !== undefined) {;
|
||||||
|
d.callId = engine.scriptManager.fetchScript(scriptId);
|
||||||
|
} else {
|
||||||
|
scriptEdit.text = "import QtQuick 2.0\nimport nymea 1.0\n\nItem {\n \n}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Qt.platform.os == "android" || Qt.platform.os == "ios") && !popupCache.shown) {
|
||||||
|
var component = Qt.createComponent(Qt.resolvedUrl("../components/MeaDialog.qml"));
|
||||||
|
var infoPopup = component.createObject(root,
|
||||||
|
{
|
||||||
|
title: qsTr("Did you know..."),
|
||||||
|
headerIcon: "../images/info.svg",
|
||||||
|
text: qsTr("nymea:app is available for all kinds of devices. In order to edit scripts we recommend to use nymea:app on your personal computer or connect a keyboard to your tablet.")
|
||||||
|
})
|
||||||
|
infoPopup.open();
|
||||||
|
popupCache.shown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings {
|
||||||
|
id: popupCache
|
||||||
|
property bool shown: false
|
||||||
|
}
|
||||||
|
|
||||||
|
header: NymeaHeader {
|
||||||
|
|
||||||
|
onBackPressed: {
|
||||||
|
if (scriptEdit.text == d.oldContent) {
|
||||||
|
pageStack.pop()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var comp = Qt.createComponent("../components/MeaDialog.qml");
|
||||||
|
var popup = comp.createObject(root, {
|
||||||
|
title: qsTr("Unsaved changes"),
|
||||||
|
text: qsTr("There are unsaved changes in the script. Do you want to discard the changes?"),
|
||||||
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
})
|
||||||
|
popup.onAccepted.connect(function() {
|
||||||
|
pageStack.pop();
|
||||||
|
});
|
||||||
|
popup.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: nameTextField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: d.script ? d.script.name : ""
|
||||||
|
placeholderText: qsTr("Script name")
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderButton {
|
||||||
|
imageSource: "../images/question.svg"
|
||||||
|
text: qsTr("Help")
|
||||||
|
onClicked: {
|
||||||
|
Qt.openUrlExternally("https://nymea.io/en/wiki/nymea/master/configuration/scripting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderButton {
|
||||||
|
imageSource: "../images/save.svg"
|
||||||
|
enabled: d.script && d.script.name !== nameTextField.text || d.oldContent !== scriptEdit.text
|
||||||
|
color: enabled ? app.accentColor : keyColor
|
||||||
|
hoverEnabled: true
|
||||||
|
ToolTip.text: qsTr("Deploy script")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
onClicked: {
|
||||||
|
if (!d.scriptId) {
|
||||||
|
d.callId = engine.scriptManager.addScript(nameTextField.text, scriptEdit.text);
|
||||||
|
} else {
|
||||||
|
print("editing script", d.scriptId)
|
||||||
|
if (d.script.name != nameTextField.text) {
|
||||||
|
engine.scriptManager.renameScript(d.scriptId, nameTextField.text)
|
||||||
|
}
|
||||||
|
if (d.oldContent != scriptEdit.text) {
|
||||||
|
d.callId = engine.scriptManager.editScript(d.scriptId, scriptEdit.text)
|
||||||
|
print("called edit", d.callId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: d
|
||||||
|
property int callId: -1
|
||||||
|
property var scriptId
|
||||||
|
property string oldContent
|
||||||
|
|
||||||
|
property Script script: engine.scriptManager.scripts.getScript(d.scriptId)
|
||||||
|
}
|
||||||
|
|
||||||
|
FontMetrics {
|
||||||
|
id: fontMetrics
|
||||||
|
font: scriptEdit.font
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: engine.scriptManager
|
||||||
|
onAddScriptReply: {
|
||||||
|
if (id == d.callId) {
|
||||||
|
d.callId = -1;
|
||||||
|
if (scriptError == "ScriptErrorNoError") {
|
||||||
|
d.scriptId = scriptId;
|
||||||
|
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."))
|
||||||
|
}
|
||||||
|
errorModel.update(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onFetchScriptReply: {
|
||||||
|
if (id == d.callId && scriptError == "ScriptErrorNoError") {
|
||||||
|
d.callId = -1;
|
||||||
|
scriptEdit.text = content;
|
||||||
|
d.oldContent = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onRenameScriptReply: {
|
||||||
|
if (id == d.callId) {
|
||||||
|
d.callId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onScriptMessage: {
|
||||||
|
if (scriptId !== d.scriptId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var str = "<font color=\"%1\">".arg(type == "ScriptMessageTypeWarning" ? app.accentColor : app.foregroundColor) + message + "</font>"
|
||||||
|
consoleOutput.append(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this a SplitView when we can use Qt 5.13
|
||||||
|
ColumnLayout {
|
||||||
|
id: content
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: scriptFlickable
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
clip: true
|
||||||
|
interactive: !completionBox.visible
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOn }
|
||||||
|
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOn }
|
||||||
|
|
||||||
|
LineNumbers {
|
||||||
|
id: lineNumbers
|
||||||
|
textArea: scriptEdit
|
||||||
|
}
|
||||||
|
|
||||||
|
TextArea.flickable: TextArea {
|
||||||
|
id: scriptEdit
|
||||||
|
leftPadding: lineNumbers.width + 2
|
||||||
|
rightPadding: 20
|
||||||
|
bottomPadding: 28
|
||||||
|
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
|
||||||
|
|
||||||
|
font.family: "Monospace"
|
||||||
|
font.pixelSize: app.extraSmallFont
|
||||||
|
selectByMouse: true
|
||||||
|
selectByKeyboard: true
|
||||||
|
|
||||||
|
onCursorPositionChanged: {
|
||||||
|
if (completionBox.visible) {
|
||||||
|
completion.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function controlPressed(event) {
|
||||||
|
return event.modifiers & Qt.ControlModifier || event.modifiers & Qt.MetaModifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: {
|
||||||
|
print("key", event.key, "Completion box visible:", completionBox.visible)
|
||||||
|
// Things to happen only when we're not autocompleting
|
||||||
|
if (!completionBox.visible) {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
completion.newLine();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Space:
|
||||||
|
if (!completionBox.visible && controlPressed(event)) {
|
||||||
|
completion.update();
|
||||||
|
completionBox.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_PageUp:
|
||||||
|
var oldSelectionStart = scriptEdit.selectionStart;
|
||||||
|
completion.moveCursor(CodeCompletion.MoveOperationPreviousLine, scriptFlickable.height / (fontMetrics.lineSpacing + 2));
|
||||||
|
if (event.modifiers & Qt.ShiftModifier) {
|
||||||
|
scriptEdit.select(oldSelectionStart, scriptEdit.cursorPosition)
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case Qt.Key_PageDown:
|
||||||
|
var oldSelectionStart = scriptEdit.selectionStart;
|
||||||
|
completion.moveCursor(CodeCompletion.MoveOperationNextLine, scriptFlickable.height / (fontMetrics.lineSpacing + 2));
|
||||||
|
if (event.modifiers & Qt.ShiftModifier) {
|
||||||
|
scriptEdit.select(oldSelectionStart, scriptEdit.cursorPosition)
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// things to happen in any case
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_BraceLeft:
|
||||||
|
completion.insertAfterCursor("}");
|
||||||
|
return;
|
||||||
|
|
||||||
|
case Qt.Key_BraceRight:
|
||||||
|
completion.closeBlock();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
completion.indent(selectionStart, selectionEnd);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Backtab:
|
||||||
|
completion.unindent(selectionStart, selectionEnd);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Period:
|
||||||
|
completion.insertBeforeCursor(".");
|
||||||
|
completionBox.show();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Plus:
|
||||||
|
if (controlPressed(event)) {
|
||||||
|
scriptEdit.font.pixelSize++;
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_Minus:
|
||||||
|
if (controlPressed(event)) {
|
||||||
|
scriptEdit.font.pixelSize--;
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Things to do only when we're autocompleting
|
||||||
|
if (completionBox.visible) {
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
completionBox.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Down:
|
||||||
|
completionBox.next();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
completionBox.previous();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
case Qt.Key_Return:
|
||||||
|
completion.complete(completionBox.currentIndex)
|
||||||
|
completionBox.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorPane {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.min(implicitHeight, root.height / 4)
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: errorsPane
|
||||||
|
anchors { fill: parent; margins: app.margins / 2 }
|
||||||
|
property string title: qsTr("Errors")
|
||||||
|
property bool clearEnabled: errorModel.count > 0
|
||||||
|
signal raise()
|
||||||
|
function clear() {
|
||||||
|
errorModel.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: errorListView
|
||||||
|
model: ListModel {
|
||||||
|
id: errorModel
|
||||||
|
property var errorLines: []
|
||||||
|
function update(errors) {
|
||||||
|
clear();
|
||||||
|
var newErrorLines = []
|
||||||
|
errors.forEach( function(error) {
|
||||||
|
var parts = error.split(":")
|
||||||
|
var line = parseInt(parts.shift());
|
||||||
|
var col = parseInt(parts.shift());
|
||||||
|
var message = parts.join(":").trim();
|
||||||
|
append({line: line, column: col, message: message})
|
||||||
|
newErrorLines.push(line);
|
||||||
|
})
|
||||||
|
errorLines = newErrorLines;
|
||||||
|
if (errorModel.count > 0) {
|
||||||
|
errorsPane.raise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getError(lineNumber) {
|
||||||
|
print("getting error for line", lineNumber, errorModel.count)
|
||||||
|
for (var i = 0; i < errorModel.count; i++) {
|
||||||
|
var entry = get(i);
|
||||||
|
print("i:", i, entry.message, entry.line)
|
||||||
|
if (entry.line === lineNumber) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Label {
|
||||||
|
width: parent.width
|
||||||
|
text: model.line + ":" + model.column + ": " + model.message
|
||||||
|
font: scriptEdit.font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
id: consolePane
|
||||||
|
anchors {fill: parent; margins: app.margins/ 2 }
|
||||||
|
property string title: qsTr("Console")
|
||||||
|
property bool clearEnabled: false
|
||||||
|
signal raise()
|
||||||
|
function clear() {
|
||||||
|
consoleOutput.text = "";
|
||||||
|
clearEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextArea {
|
||||||
|
id: consoleOutput
|
||||||
|
onTextChanged: {
|
||||||
|
consolePane.raise();
|
||||||
|
print("text:", text)
|
||||||
|
consolePane.clearEnabled = true
|
||||||
|
}
|
||||||
|
selectByMouse: true
|
||||||
|
font: scriptEdit.font
|
||||||
|
textFormat: Qt.RichText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletionBox {
|
||||||
|
id: completionBox
|
||||||
|
property var editorPosition: scriptFlickable.mapToItem(root, 0, 0)
|
||||||
|
property int scrollOffsetX: scriptFlickable.contentX + scriptFlickable.originX
|
||||||
|
property int scrollOffsetY: scriptFlickable.contentY + scriptFlickable.originY
|
||||||
|
property int cursorXOnPage: scriptEdit.cursorRectangle.x + editorPosition.x - scrollOffsetX
|
||||||
|
property int cursorYOnPage: scriptEdit.cursorRectangle.y + editorPosition.y - scrollOffsetY
|
||||||
|
property int cursorHeight: scriptEdit.cursorRectangle.height
|
||||||
|
x: cursorXOnPage - Math.max(0, cursorXOnPage + width - root.width)
|
||||||
|
y: cursorYOnPage + cursorHeight + height < content.height ?
|
||||||
|
cursorYOnPage + cursorHeight
|
||||||
|
: cursorYOnPage - height
|
||||||
|
|
||||||
|
model: completion.model
|
||||||
|
textArea: scriptEdit
|
||||||
|
font: scriptEdit.font
|
||||||
|
onComplete: {
|
||||||
|
completion.complete(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptSyntaxHighlighter {
|
||||||
|
id: syntax
|
||||||
|
document: scriptEdit.textDocument
|
||||||
|
backgroundColor: app.backgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeCompletion {
|
||||||
|
id: completion
|
||||||
|
engine: _engine
|
||||||
|
document: scriptEdit.textDocument
|
||||||
|
cursorPosition: scriptEdit.cursorPosition
|
||||||
|
onCursorPositionChanged: scriptEdit.cursorPosition = cursorPosition
|
||||||
|
onHint: completionBox.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
BusyOverlay {
|
||||||
|
shown: d.callId != -1
|
||||||
|
}
|
||||||
|
}
|
||||||
69
nymea-app/ui/magic/ScriptsPage.qml
Normal file
69
nymea-app/ui/magic/ScriptsPage.qml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import Nymea 1.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
Page {
|
||||||
|
header: NymeaHeader {
|
||||||
|
text: qsTr("Scripts")
|
||||||
|
onBackPressed: pageStack.pop();
|
||||||
|
|
||||||
|
HeaderButton {
|
||||||
|
text: qsTr("Add new script")
|
||||||
|
imageSource: "../images/add.svg"
|
||||||
|
onClicked: {
|
||||||
|
pageStack.push("ScriptEditor.qml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: d
|
||||||
|
property int pendingAction: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: engine.scriptManager
|
||||||
|
onRemoveScriptReply: {
|
||||||
|
if (id == d.pendingAction) {
|
||||||
|
d.pendingAction = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
anchors.fill: parent
|
||||||
|
model: engine.scriptManager.scripts
|
||||||
|
delegate: NymeaListItemDelegate {
|
||||||
|
width: parent.width
|
||||||
|
text: model.name
|
||||||
|
iconName: "../images/script.svg"
|
||||||
|
canDelete: true
|
||||||
|
onClicked: {
|
||||||
|
pageStack.push("ScriptEditor.qml", {scriptId: model.id});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteClicked: {
|
||||||
|
print("removing script", model.id)
|
||||||
|
d.pendingAction = engine.scriptManager.removeScript(model.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmptyViewPlaceholder {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
title: qsTr("No scripts are installed yet.")
|
||||||
|
text: qsTr("Press \"Add script\" to get started.")
|
||||||
|
imageSource: "../images/script.svg"
|
||||||
|
buttonText: qsTr("Add script")
|
||||||
|
visible: engine.scriptManager.scripts.count === 0
|
||||||
|
onButtonClicked: {
|
||||||
|
pageStack.push("ScriptEditor.qml");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BusyOverlay {
|
||||||
|
id: busyOverlay
|
||||||
|
visible: d.pendingAction != -1
|
||||||
|
}
|
||||||
|
}
|
||||||
168
nymea-app/ui/magic/scripting/CompletionBox.qml
Normal file
168
nymea-app/ui/magic/scripting/CompletionBox.qml
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import QtQuick 2.2
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import Nymea 1.0
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
import "../../components"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
border.width: 1
|
||||||
|
border.color: app.foregroundColor
|
||||||
|
color: app.backgroundColor
|
||||||
|
height: (Math.min(model.count, 10) * d.entryHeight) + (border.width * 2)
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
visible: model.count > 0 && !d.hidden
|
||||||
|
&& (model.filter.length >= 3 || d.manuallyInvoked)
|
||||||
|
|
||||||
|
property TextArea textArea: null
|
||||||
|
property CompletionModel model: null
|
||||||
|
property alias font: dummyLabel.font
|
||||||
|
|
||||||
|
signal complete(int index)
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.model
|
||||||
|
onCountChanged: {
|
||||||
|
d.hidden = false;
|
||||||
|
d.currentIndex = 0;
|
||||||
|
if (root.model.count == 0) {
|
||||||
|
d.manuallyInvoked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property alias currentIndex: d.currentIndex
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
d.currentIndex = (d.currentIndex + 1) % root.model.count
|
||||||
|
}
|
||||||
|
|
||||||
|
function previous() {
|
||||||
|
d.currentIndex--;
|
||||||
|
if (d.currentIndex < 0) {
|
||||||
|
d.currentIndex = root.model.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (root.model.count > 1) {
|
||||||
|
d.hidden = false;
|
||||||
|
d.manuallyInvoked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
d.hidden = true;
|
||||||
|
d.manuallyInvoked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
listView.positionViewAtIndex(currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: dummyLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: d
|
||||||
|
property int entryHeight: dummyLabel.font.pixelSize + 6
|
||||||
|
|
||||||
|
property int currentIndex: 0
|
||||||
|
property bool hidden: false
|
||||||
|
property bool manuallyInvoked: false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root.border.width
|
||||||
|
model: root.model
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: root.model.count > 10 ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
height: d.entryHeight
|
||||||
|
width: parent.width
|
||||||
|
color: index == root.currentIndex ? app.accentColor : "transparent"
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredHeight: d.entryHeight
|
||||||
|
Layout.preferredWidth: height
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
height: root.font.pixelSize * .6
|
||||||
|
width: height
|
||||||
|
border.width: 1
|
||||||
|
border.color: "black"
|
||||||
|
visible: !entryIcon.visible
|
||||||
|
color: {
|
||||||
|
switch (model.decoration) {
|
||||||
|
case "type":
|
||||||
|
return "#55fc49";
|
||||||
|
case "keyword":
|
||||||
|
return "yellow";
|
||||||
|
case "property":
|
||||||
|
return "#ff5555";
|
||||||
|
case "method":
|
||||||
|
return "blue";
|
||||||
|
case "event":
|
||||||
|
return "magenta";
|
||||||
|
case "id":
|
||||||
|
return "turquise";
|
||||||
|
default:
|
||||||
|
return "transparent";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ColorIcon {
|
||||||
|
id: entryIcon
|
||||||
|
height: root.font.pixelSize
|
||||||
|
width: height
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: name != ""
|
||||||
|
color: root.currentIndex == index ? app.backgroundColor : app.accentColor
|
||||||
|
name: {
|
||||||
|
switch (model.decoration) {
|
||||||
|
case "thing":
|
||||||
|
return app.interfacesToIcon(model.decorationProperty.split(","))
|
||||||
|
case "eventType":
|
||||||
|
return Qt.resolvedUrl("../../images/event.svg")
|
||||||
|
case "stateType":
|
||||||
|
return Qt.resolvedUrl("../../images/state.svg")
|
||||||
|
case "actionType":
|
||||||
|
return Qt.resolvedUrl("../../images/action.svg")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: model.displayText
|
||||||
|
color: app.foregroundColor
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font: root.font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
root.complete(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
nymea-app/ui/magic/scripting/EditorPane.qml
Normal file
122
nymea-app/ui/magic/scripting/EditorPane.qml
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import QtQuick 2.2
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.1
|
||||||
|
import "../../components"
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: pane
|
||||||
|
implicitHeight: shown ? 40 + 10 * app.smallFont : 25
|
||||||
|
|
||||||
|
readonly property bool shown: (shownOverride === "auto" && autoWouldShow)
|
||||||
|
|| shownOverride == "shown"
|
||||||
|
readonly property alias autoWouldShow: d.autoWouldShow
|
||||||
|
property string shownOverride: "auto" // "shown", "hidden"
|
||||||
|
|
||||||
|
default property alias panels: contentContainer.data
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: d
|
||||||
|
property bool autoWouldShow: false
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
RowLayout {
|
||||||
|
id: panelHeader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.rightMargin: app.margins
|
||||||
|
Layout.leftMargin: app.margins
|
||||||
|
Layout.maximumHeight: 24
|
||||||
|
Layout.minimumHeight: 24
|
||||||
|
|
||||||
|
TabBar {
|
||||||
|
id: panelTabs
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: contentContainer.data
|
||||||
|
|
||||||
|
TabButton {
|
||||||
|
implicitHeight: panelHeader.height
|
||||||
|
background: Rectangle {
|
||||||
|
implicitWidth: 200
|
||||||
|
implicitHeight: panelHeader.height
|
||||||
|
color: app.backgroundColor
|
||||||
|
Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: contentContainer.data[index].title
|
||||||
|
font.pixelSize: app.smallFont
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Binding {
|
||||||
|
target: contentContainer.data[index]
|
||||||
|
property: "visible"
|
||||||
|
value: panelTabs.currentIndex === index
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: contentContainer.data[index]
|
||||||
|
onRaise: {
|
||||||
|
panelTabs.currentIndex = index
|
||||||
|
d.autoWouldShow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorIcon {
|
||||||
|
name: "../images/edit-clear.svg"
|
||||||
|
enabled: contentContainer.data[panelTabs.currentIndex].clearEnabled
|
||||||
|
color: enabled ? app.accentColor : keyColor
|
||||||
|
Layout.preferredHeight: app.iconSize / 2
|
||||||
|
Layout.preferredWidth: height
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -5
|
||||||
|
onClicked: contentContainer.data[panelTabs.currentIndex].clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorIcon {
|
||||||
|
name: pane.shown ? "../images/down.svg" : "../images/up.svg"
|
||||||
|
Layout.preferredHeight: app.iconSize / 2
|
||||||
|
Layout.preferredWidth: height
|
||||||
|
color: app.accentColor
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -5
|
||||||
|
onClicked: {
|
||||||
|
if (pane.shown) {
|
||||||
|
if (pane.autoWouldShow) {
|
||||||
|
pane.shownOverride = "hidden"
|
||||||
|
} else {
|
||||||
|
pane.shownOverride = "auto"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pane.autoWouldShow) {
|
||||||
|
pane.shownOverride = "auto"
|
||||||
|
} else {
|
||||||
|
pane.shownOverride = "shown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThinDivider {}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: contentContainer
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
nymea-app/ui/magic/scripting/LineNumbers.qml
Normal file
69
nymea-app/ui/magic/scripting/LineNumbers.qml
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import QtQuick 2.4
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property TextArea textArea: null
|
||||||
|
|
||||||
|
FontMetrics {
|
||||||
|
id: fontMetrics
|
||||||
|
font: textArea.font
|
||||||
|
}
|
||||||
|
TextMetrics {
|
||||||
|
id: textMetrics
|
||||||
|
font: textArea.font
|
||||||
|
text: {
|
||||||
|
var digits = 1;
|
||||||
|
var tmp = textArea.lineCount;
|
||||||
|
while (tmp >= 10) {
|
||||||
|
digits++;
|
||||||
|
tmp /= 10;
|
||||||
|
}
|
||||||
|
var str = ""
|
||||||
|
for (var i = 0; i < digits; i++) {
|
||||||
|
str += "0"
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: textMetrics.advanceWidth + app.margins / 2
|
||||||
|
height: root.textArea.height - 10
|
||||||
|
color: (app.backgroundColor.r * 0.2126 + app.backgroundColor.g * 0.7152 + app.backgroundColor.b * 0.0722) * 255 < 128 ? "#202020" : "#e0e0e0"
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: lineNumbersColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 8
|
||||||
|
Repeater {
|
||||||
|
model: root.textArea.lineCount
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: lineNumberDelegate
|
||||||
|
width: parent.width
|
||||||
|
height: root.textArea.contentHeight / root.textArea.lineCount
|
||||||
|
color: hasError ? "#FF0000" : "transparent"
|
||||||
|
readonly property bool hasError: errorModel.errorLines.indexOf(index + 1) >= 0
|
||||||
|
Label {
|
||||||
|
id: lineNumber
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 3
|
||||||
|
text: index + 1
|
||||||
|
font.pixelSize: root.textArea.font.pixelSize
|
||||||
|
font.family: root.textArea.font.family
|
||||||
|
font.weight: Font.Light
|
||||||
|
color: lineNumberDelegate.hasError ? "#FFFFFF" : "#808080"
|
||||||
|
}
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
ToolTip.visible: lineNumberDelegate.hasError && containsMouse
|
||||||
|
ToolTip.text: hasError ? errorModel.getError(index + 1).message : ""
|
||||||
|
property string bla: hasError ? ".." : ""
|
||||||
|
onBlaChanged: print("**", errorModel.getError(index + 1).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user