More work on the script editor
This commit is contained in:
parent
0c6d75cdd0
commit
687912a82c
@ -70,6 +70,8 @@
|
||||
#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"
|
||||
|
||||
@ -239,7 +241,8 @@ void registerQmlTypes() {
|
||||
qmlRegisterUncreatableType<Scripts>(uri, 1, 0, "Scripts", "Getit from ScriptManager");
|
||||
qmlRegisterUncreatableType<Script>(uri, 1, 0, "Script", "Getit from Scripts");
|
||||
qmlRegisterType<ScriptSyntaxHighlighter>(uri, 1, 0, "ScriptSyntaxHighlighter");
|
||||
qmlRegisterUncreatableType<CompletionProxyModel>(uri, 1, 0, "CompletionProxyModel", "Get it from ScriptSyntaxHighlighter");
|
||||
qmlRegisterType<CodeCompletion>(uri, 1, 0, "CodeCompletion");
|
||||
qmlRegisterUncreatableType<CompletionProxyModel>(uri, 1, 0, "CompletionModel", "Get it from ScriptSyntaxHighlighter");
|
||||
}
|
||||
|
||||
#endif // LIBNYMEAAPPCORE_H
|
||||
|
||||
@ -48,6 +48,8 @@ SOURCES += \
|
||||
devicediscovery.cpp \
|
||||
models/packagesfiltermodel.cpp \
|
||||
models/taglistmodel.cpp \
|
||||
scripting/codecompletion.cpp \
|
||||
scripting/completionmodel.cpp \
|
||||
scriptmanager.cpp \
|
||||
scriptsyntaxhighlighter.cpp \
|
||||
vendorsproxy.cpp \
|
||||
@ -113,6 +115,8 @@ HEADERS += \
|
||||
devicediscovery.h \
|
||||
models/packagesfiltermodel.h \
|
||||
models/taglistmodel.h \
|
||||
scripting/codecompletion.h \
|
||||
scripting/completionmodel.h \
|
||||
scriptmanager.h \
|
||||
scriptsyntaxhighlighter.h \
|
||||
vendorsproxy.h \
|
||||
|
||||
533
libnymea-app-core/scripting/codecompletion.cpp
Normal file
533
libnymea-app-core/scripting/codecompletion.cpp
Normal file
@ -0,0 +1,533 @@
|
||||
#include "codecompletion.h"
|
||||
|
||||
#include "completionmodel.h"
|
||||
#include "engine.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QQuickItem>
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlock>
|
||||
|
||||
CodeCompletion::CodeCompletion(QObject *parent):
|
||||
QObject(parent)
|
||||
{
|
||||
registerType<QQuickItem>("Item");
|
||||
m_classes.insert("DeviceAction", {"id", "deviceId", "actionTypeId", "actionName"});
|
||||
m_classes.insert("DeviceState", {"id", "deviceId", "stateTypeId", "stateName", "value", "onValueChanged"});
|
||||
m_classes.insert("DeviceEvent", {"id", "deviceId", "eventTypeId", "eventName", "onTriggered"});
|
||||
m_classes.insert("Timer", {"id", "interval", "running", "repeat", "onTriggered"});
|
||||
|
||||
m_genericSyntax.insert("property", "property ");
|
||||
|
||||
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){
|
||||
qDebug() << "text cursor changed" << cursor.position();
|
||||
m_cursor = cursor;
|
||||
update();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
int CodeCompletion::cursorPosition() const
|
||||
{
|
||||
return m_cursor.position();
|
||||
}
|
||||
|
||||
void CodeCompletion::setCursorPosition(int position)
|
||||
{
|
||||
qDebug() << "setCursorPos" << position << m_cursor.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();
|
||||
|
||||
QString blockText = m_cursor.block().text();
|
||||
|
||||
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(), true, true));
|
||||
|
||||
}
|
||||
blockText.remove(QRegExp(".*deviceId: \""));
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(blockText);
|
||||
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(), stateType->name(), true, true));
|
||||
}
|
||||
blockText.remove(QRegExp(".*stateTypeId: \""));
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(blockText);
|
||||
return;
|
||||
}
|
||||
|
||||
QRegExp stateNameExp(".*stateName: \"[a-zA-Z0-9-]*");
|
||||
if (stateNameExp.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->name(), stateType->name(), true, false));
|
||||
}
|
||||
blockText.remove(QRegExp(".*stateName: \""));
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(blockText);
|
||||
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(), actionType->name(), true, true));
|
||||
}
|
||||
blockText.remove(QRegExp(".*actionTypeId: \""));
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(blockText);
|
||||
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(), true, false));
|
||||
}
|
||||
blockText.remove(QRegExp(".*actionName: \""));
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(blockText);
|
||||
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(), eventType->name(), true, true));
|
||||
}
|
||||
blockText.remove(QRegExp(".*eventTypeId: \""));
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(blockText);
|
||||
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(), true, false));
|
||||
}
|
||||
blockText.remove(QRegExp(".*eventName: \""));
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(blockText);
|
||||
return;
|
||||
}
|
||||
|
||||
QRegExp importExp("imp(o|or)?");
|
||||
if (importExp.exactMatch(blockText)) {
|
||||
entries.append(CompletionModel::Entry("import ", "import"));
|
||||
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]+:[ 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;
|
||||
// Find all ids in the doc
|
||||
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));
|
||||
}
|
||||
tmp.movePosition(QTextCursor::NextWord);
|
||||
}
|
||||
m_model->update(entries);
|
||||
m_proxy->setFilter(word);
|
||||
return;
|
||||
}
|
||||
|
||||
QRegExp lValueStartExp(" *[a-zA-Z0-9]*");
|
||||
if (lValueStartExp.exactMatch(blockText)) {
|
||||
qDebug() << "matching";
|
||||
QTextCursor blockStartCursor = m_document->textDocument()->find("{", m_cursor, QTextDocument::FindBackward);
|
||||
QTextCursor blockEndCursor = m_document->textDocument()->find("}", m_cursor, QTextDocument::FindBackward);
|
||||
while (!blockEndCursor.isNull() && blockEndCursor.position() > blockStartCursor.position()) {
|
||||
blockStartCursor = m_document->textDocument()->find("{", blockStartCursor, QTextDocument::FindBackward);
|
||||
blockEndCursor = m_document->textDocument()->find("}", blockEndCursor, QTextDocument::FindBackward);
|
||||
}
|
||||
QString className = blockStartCursor.block().text();
|
||||
className.remove(QRegExp(" *\\{"));
|
||||
while (className.contains(" ")) {
|
||||
className.remove(QRegExp(".* "));
|
||||
}
|
||||
|
||||
// If we're inside a class, add properties
|
||||
if (!className.isEmpty()) {
|
||||
foreach (const QString &s, m_classes.value(className)) {
|
||||
entries.append(CompletionModel::Entry(s + ": ", s));
|
||||
}
|
||||
}
|
||||
|
||||
// Always append class names
|
||||
foreach (const QString &s, m_classes.keys()) {
|
||||
entries.append(CompletionModel::Entry(s + " {", s));
|
||||
}
|
||||
|
||||
// Add generic syntax
|
||||
foreach (const QString &s, m_genericSyntax.keys()) {
|
||||
entries.append(CompletionModel::Entry(m_genericSyntax.value(s), s));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
qDebug() << "Block strats at" << blockStart.position();
|
||||
|
||||
info.name = blockStart.block().text();
|
||||
info.name.remove(QRegExp(" *\\{"));
|
||||
while (info.name.contains(" ")) {
|
||||
info.name.remove(QRegExp(".* "));
|
||||
}
|
||||
|
||||
qDebug() << "Block name:" << info.name;
|
||||
|
||||
while (blockStart.position() < position) {
|
||||
qDebug() << "current pos:" << blockStart.position() << blockStart.block().text();
|
||||
QTextCursor tmp = m_document->textDocument()->find("\n", blockStart);
|
||||
foreach (const QString &statement, blockStart.block().text().split(";")) {
|
||||
qDebug() << "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);
|
||||
}
|
||||
blockStart.movePosition(QTextCursor::NextBlock);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
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);
|
||||
QString textToInsert = entry.text;
|
||||
|
||||
if (entry.addTrailingQuote) {
|
||||
textToInsert.append("\"");
|
||||
}
|
||||
if (entry.addComment) {
|
||||
textToInsert.append(" // " + entry.displayText);
|
||||
}
|
||||
// textToInsert.append("\n");
|
||||
m_cursor.select(QTextCursor::WordUnderCursor);
|
||||
m_cursor.removeSelectedText();
|
||||
m_cursor.insertText(textToInsert);
|
||||
if (textToInsert.endsWith("{")) {
|
||||
insertAfterCursor("}");
|
||||
}
|
||||
}
|
||||
|
||||
void CodeCompletion::newLine()
|
||||
{
|
||||
qDebug() << "Newline" << m_cursor.position();
|
||||
QString line = m_cursor.block().text();
|
||||
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::insertAfterCursor(const QString &text)
|
||||
{
|
||||
m_cursor.insertText(text);
|
||||
m_cursor.movePosition(QTextCursor::PreviousCharacter);
|
||||
emit cursorPositionChanged();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void CodeCompletion::registerType(const QString &qmlName)
|
||||
{
|
||||
QMetaObject metaObject = T::staticMetaObject;
|
||||
QStringList properties;
|
||||
for (int i = 0; i < metaObject.propertyCount(); i++) {
|
||||
qDebug() << "Adding prop" << metaObject.property(i).name() << metaObject.property(i).type();
|
||||
if (metaObject.property(i).isWritable()) {
|
||||
properties.append(metaObject.property(i).name());
|
||||
}
|
||||
}
|
||||
m_classes.insert(qmlName, properties);
|
||||
}
|
||||
77
libnymea-app-core/scripting/codecompletion.h
Normal file
77
libnymea-app-core/scripting/codecompletion.h
Normal file
@ -0,0 +1,77 @@
|
||||
#ifndef CODECOMPLETION_H
|
||||
#define CODECOMPLETION_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QTextCursor>
|
||||
#include <QHash>
|
||||
|
||||
class Engine;
|
||||
class CompletionModel;
|
||||
class CompletionProxyModel;
|
||||
|
||||
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:
|
||||
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 insertAfterCursor(const QString &text);
|
||||
|
||||
signals:
|
||||
void engineChanged();
|
||||
void documentChanged();
|
||||
void cursorPositionChanged();
|
||||
void currentWordChanged();
|
||||
|
||||
private:
|
||||
struct BlockInfo {
|
||||
QString name;
|
||||
QHash<QString, QString> properties;
|
||||
};
|
||||
|
||||
BlockInfo getBlockInfo(int postition);
|
||||
|
||||
template<typename T> void registerType(const QString &qmlName);
|
||||
|
||||
private:
|
||||
Engine *m_engine = nullptr;
|
||||
QQuickTextDocument* m_document = nullptr;
|
||||
CompletionModel *m_model = nullptr;
|
||||
CompletionProxyModel *m_proxy = nullptr;
|
||||
|
||||
QTextCursor m_cursor;
|
||||
|
||||
QHash<QString, QStringList> m_classes;
|
||||
QHash<QString, QString> m_genericSyntax;
|
||||
|
||||
};
|
||||
|
||||
#endif // CODECOMPLETION_H
|
||||
92
libnymea-app-core/scripting/completionmodel.cpp
Normal file
92
libnymea-app-core/scripting/completionmodel.cpp
Normal file
@ -0,0 +1,92 @@
|
||||
#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");
|
||||
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;
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (m_filter != filter) {
|
||||
qDebug() << "Setting filter" << filter;
|
||||
m_filter = filter;
|
||||
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 (!entry.displayText.startsWith(m_filter) && !entry.text.startsWith(m_filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
65
libnymea-app-core/scripting/completionmodel.h
Normal file
65
libnymea-app-core/scripting/completionmodel.h
Normal file
@ -0,0 +1,65 @@
|
||||
#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, bool addTrailingQuote = false, bool addComment = false)
|
||||
: text(text), displayText(displayText), addTrailingQuote(addTrailingQuote), addComment(addComment) {}
|
||||
Entry(const QString &text): text(text), displayText(text) {}
|
||||
QString text;
|
||||
QString displayText;
|
||||
bool addTrailingQuote = false;
|
||||
bool addComment = false;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int source_row, const QModelIndex &/*source_parent*/) const override;
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
void filterChanged();
|
||||
|
||||
private:
|
||||
CompletionModel *m_model = nullptr;
|
||||
QString m_filter;
|
||||
|
||||
};
|
||||
|
||||
#endif // COMPLETIONMODEL_H
|
||||
@ -4,10 +4,12 @@
|
||||
#include "types/scripts.h"
|
||||
|
||||
ScriptManager::ScriptManager(JsonRpcClient *jsonClient, QObject *parent):
|
||||
QObject(parent),
|
||||
JsonHandler(parent),
|
||||
m_client(jsonClient)
|
||||
{
|
||||
m_scripts = new Scripts(this);
|
||||
|
||||
m_client->registerNotificationHandler(this, "onNotificationReceived");
|
||||
}
|
||||
|
||||
void ScriptManager::init()
|
||||
@ -16,6 +18,11 @@ void ScriptManager::init()
|
||||
m_client->sendCommand("Scripts.GetScripts", QVariantMap(), this, "onScriptsFetched");
|
||||
}
|
||||
|
||||
QString ScriptManager::nameSpace() const
|
||||
{
|
||||
return "Scripts";
|
||||
}
|
||||
|
||||
Scripts *ScriptManager::scripts() const
|
||||
{
|
||||
return m_scripts;
|
||||
@ -34,6 +41,7 @@ int ScriptManager::editScript(const QUuid &id, const QString &content)
|
||||
QVariantMap params;
|
||||
params.insert("id", id);
|
||||
params.insert("content", content);
|
||||
qDebug() << "Calling EditScript" << content;
|
||||
return m_client->sendCommand("Scripts.EditScript", params, this, "onScriptEdited");
|
||||
}
|
||||
|
||||
@ -44,6 +52,13 @@ int ScriptManager::removeScript(const QUuid &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)
|
||||
{
|
||||
qDebug() << "scripts fetched" << params;
|
||||
@ -57,6 +72,14 @@ void ScriptManager::onScriptsFetched(const QVariantMap ¶ms)
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptManager::onScriptFetched(const QVariantMap ¶ms)
|
||||
{
|
||||
qDebug() << "Script fetched" << params;
|
||||
emit scriptFetched(params.value("id").toInt(),
|
||||
params.value("params").toMap().value("scriptError").toString(),
|
||||
params.value("params").toMap().value("content").toString());
|
||||
}
|
||||
|
||||
void ScriptManager::onScriptAdded(const QVariantMap ¶ms)
|
||||
{
|
||||
qDebug() << "Script added" << params;
|
||||
@ -81,3 +104,13 @@ void ScriptManager::onScriptRemoved(const QVariantMap ¶ms)
|
||||
{
|
||||
emit scriptRemoved(params.value("id").toInt(), params.value("params").toMap().value("scriptError").toString());
|
||||
}
|
||||
|
||||
void ScriptManager::onNotificationReceived(const QVariantMap ¶ms)
|
||||
{
|
||||
qDebug() << "noticication" << params;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
class Scripts;
|
||||
|
||||
class ScriptManager : public QObject
|
||||
class ScriptManager : public JsonHandler
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(Scripts* scripts READ scripts CONSTANT)
|
||||
@ -17,24 +17,32 @@ public:
|
||||
|
||||
void init();
|
||||
|
||||
QString nameSpace() const override;
|
||||
|
||||
Scripts *scripts() const;
|
||||
|
||||
public slots:
|
||||
int addScript(const QString &content);
|
||||
int editScript(const QUuid &id, const QString &content);
|
||||
int removeScript(const QUuid &id);
|
||||
int fetchScript(const QUuid &id);
|
||||
|
||||
signals:
|
||||
void scriptAdded(int id, const QString &scriptError, const QUuid &scriptId, const QStringList &errors);
|
||||
void scriptEdited(int id, const QString &scriptError, const QStringList &errors);
|
||||
void scriptRemoved(int id, const QString &scriptError);
|
||||
void scriptFetched(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 onScriptRemoved(const QVariantMap ¶ms);
|
||||
|
||||
void onNotificationReceived(const QVariantMap ¶ms);
|
||||
private:
|
||||
JsonRpcClient* m_client = nullptr;
|
||||
Scripts *m_scripts = nullptr;
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <QDebug>
|
||||
#include <QMetaObject>
|
||||
#include <QTextDocumentFragment>
|
||||
#include <QQuickItem>
|
||||
|
||||
class ScriptSyntaxHighlighterPrivate: public QSyntaxHighlighter
|
||||
{
|
||||
@ -14,6 +15,7 @@ class ScriptSyntaxHighlighterPrivate: public QSyntaxHighlighter
|
||||
public:
|
||||
ScriptSyntaxHighlighterPrivate(QObject *parent);
|
||||
|
||||
void update(bool dark);
|
||||
protected:
|
||||
void highlightBlock(const QString &text) override;
|
||||
|
||||
@ -34,35 +36,12 @@ private:
|
||||
QTextCharFormat format;
|
||||
};
|
||||
QVector<HighlightingRule> highlightingRules;
|
||||
|
||||
QTextCharFormat keywordFormat;
|
||||
QTextCharFormat propertyFormat;
|
||||
QTextCharFormat lookupFormat;
|
||||
QTextCharFormat quotationFormat;
|
||||
QTextCharFormat itemFormat;
|
||||
QTextCharFormat cppObjectFormat;
|
||||
};
|
||||
|
||||
ScriptSyntaxHighlighter::ScriptSyntaxHighlighter(QObject *parent) : QObject(parent)
|
||||
{
|
||||
m_completionModel = new CompletionModel(this);
|
||||
m_proxyModel = new CompletionProxyModel(m_completionModel, this);
|
||||
m_highlighter = new ScriptSyntaxHighlighterPrivate(this);
|
||||
|
||||
m_classes.insert("Action", {"id", "deviceId", "actionTypeId", "actionName"});
|
||||
}
|
||||
|
||||
Engine *ScriptSyntaxHighlighter::engine() const
|
||||
{
|
||||
return m_engine;
|
||||
}
|
||||
|
||||
void ScriptSyntaxHighlighter::setEngine(Engine *engine)
|
||||
{
|
||||
if (m_engine != engine) {
|
||||
m_engine = engine;
|
||||
emit engineChanged();
|
||||
}
|
||||
m_highlighter->update(false);
|
||||
}
|
||||
|
||||
QQuickTextDocument *ScriptSyntaxHighlighter::document() const
|
||||
@ -75,241 +54,105 @@ void ScriptSyntaxHighlighter::setDocument(QQuickTextDocument *document)
|
||||
if (m_document != document) {
|
||||
m_document = document;
|
||||
m_highlighter->setDocument(m_document->textDocument());
|
||||
|
||||
connect(document->textDocument(), &QTextDocument::cursorPositionChanged, this, &ScriptSyntaxHighlighter::onCursorPositionChanged);
|
||||
emit documentChanged();
|
||||
}
|
||||
}
|
||||
|
||||
int ScriptSyntaxHighlighter::cursorPosition() const
|
||||
QColor ScriptSyntaxHighlighter::backgroundColor() const
|
||||
{
|
||||
return m_currentCursor.position();
|
||||
return m_backgroundColor;
|
||||
}
|
||||
|
||||
void ScriptSyntaxHighlighter::setCursorPosition(int cursorPosition)
|
||||
void ScriptSyntaxHighlighter::setBackgroundColor(const QColor &backgroundColor)
|
||||
{
|
||||
if (m_currentCursor.position() != cursorPosition) {
|
||||
m_currentCursor.setPosition(cursorPosition);
|
||||
// emit cursorPositionChanged();
|
||||
onCursorPositionChanged(m_currentCursor);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
CompletionProxyModel *ScriptSyntaxHighlighter::completionModel() const
|
||||
{
|
||||
return m_proxyModel;
|
||||
}
|
||||
|
||||
void ScriptSyntaxHighlighter::complete(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_proxyModel->rowCount()) {
|
||||
qWarning() << "Invalid index for completion";
|
||||
return;
|
||||
}
|
||||
CompletionModel::Entry entry = m_proxyModel->get(index);
|
||||
QString textToInsert = entry.text;
|
||||
|
||||
if (entry.addTrailingQuote) {
|
||||
textToInsert.append("\"");
|
||||
}
|
||||
if (entry.addComment) {
|
||||
textToInsert.append(" // " + entry.displayText);
|
||||
}
|
||||
// textToInsert.append("\n");
|
||||
m_currentCursor.select(QTextCursor::WordUnderCursor);
|
||||
m_currentCursor.removeSelectedText();
|
||||
m_currentCursor.insertText(textToInsert);
|
||||
}
|
||||
|
||||
void ScriptSyntaxHighlighter::newLine()
|
||||
{
|
||||
QString line = m_currentCursor.block().text();
|
||||
QString trimmedLine = line;
|
||||
trimmedLine.remove(QRegExp("^[ ]+"));
|
||||
int indent = line.length() - trimmedLine.length();
|
||||
|
||||
m_currentCursor.insertText(QString("\n").leftJustified(indent + 1, ' '));
|
||||
if (m_currentCursor.block().previous().text().endsWith("{")) {
|
||||
m_document->textDocument()->indentWidth();
|
||||
m_currentCursor.insertText(" ");
|
||||
m_currentCursor.insertText(QString("\n").leftJustified(indent + 1, ' '));
|
||||
m_currentCursor.insertText("}");
|
||||
m_currentCursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor, 1);
|
||||
m_currentCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::MoveAnchor, 1);
|
||||
emit cursorPositionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptSyntaxHighlighter::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 ScriptSyntaxHighlighter::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 ScriptSyntaxHighlighter::closeBlock()
|
||||
{
|
||||
m_currentCursor.insertText("}");
|
||||
if (m_currentCursor.block().text().trimmed() == "}") {
|
||||
unindent(m_currentCursor.position(), m_currentCursor.position());
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptSyntaxHighlighter::onCursorPositionChanged(const QTextCursor &cursor)
|
||||
{
|
||||
m_currentCursor = cursor;
|
||||
QTextCursor word = cursor;
|
||||
word.select(QTextCursor::WordUnderCursor);
|
||||
|
||||
QString blockText = cursor.block().text();
|
||||
m_completionModel->clear();
|
||||
m_proxyModel->setFilter(QString());
|
||||
if (!m_engine) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
m_completionModel->append(CompletionModel::Entry(dev->id().toString(), dev->name(), true, true));
|
||||
|
||||
}
|
||||
blockText.remove(QRegExp(".*deviceId: \""));
|
||||
m_proxyModel->setFilter(blockText);
|
||||
return;
|
||||
}
|
||||
|
||||
QRegExp importExp("imp(o|or)?");
|
||||
if (importExp.exactMatch(blockText)) {
|
||||
m_completionModel->append(CompletionModel::Entry("import ", "import"));
|
||||
m_proxyModel->setFilter(blockText);
|
||||
return;
|
||||
}
|
||||
|
||||
QRegExp importExp2("import [a-zA-Z]*");
|
||||
if (importExp2.exactMatch(blockText)) {
|
||||
m_completionModel->append(CompletionModel::Entry("QtQuick 2.0"));
|
||||
m_completionModel->append(CompletionModel::Entry("nymea 1.0"));
|
||||
blockText.remove("import ");
|
||||
m_proxyModel->setFilter(blockText);
|
||||
return;
|
||||
}
|
||||
|
||||
QRegExp expressionStartExp(" *[a-zA-Z0-9]*");
|
||||
if (expressionStartExp.exactMatch(blockText)) {
|
||||
QTextCursor blockStartCursor = m_document->textDocument()->find("{", m_currentCursor, QTextDocument::FindBackward);
|
||||
QTextCursor blockEndCursor = m_document->textDocument()->find("}", m_currentCursor, QTextDocument::FindBackward);
|
||||
while (!blockEndCursor.isNull() && blockEndCursor.position() > blockStartCursor.position()) {
|
||||
blockStartCursor = m_document->textDocument()->find("{", blockStartCursor, QTextDocument::FindBackward);
|
||||
blockEndCursor = m_document->textDocument()->find("}", blockEndCursor, QTextDocument::FindBackward);
|
||||
}
|
||||
QString className = blockStartCursor.block().text();
|
||||
className.remove(QRegExp(" *\\{"));
|
||||
while (className.contains(" ")) {
|
||||
className.remove(QRegExp(".* "));
|
||||
}
|
||||
qDebug() << "ClassName" << className << m_classes.value(className);
|
||||
foreach (const QString &s, m_classes.value(className)) {
|
||||
m_completionModel->append(CompletionModel::Entry(s + ": ", s));
|
||||
}
|
||||
blockText.remove(QRegExp(".* "));
|
||||
m_proxyModel->setFilter(blockText);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScriptSyntaxHighlighterPrivate::ScriptSyntaxHighlighterPrivate(QObject *parent):
|
||||
QSyntaxHighlighter(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ScriptSyntaxHighlighterPrivate::update(bool dark)
|
||||
{
|
||||
HighlightingRule rule;
|
||||
QTextCharFormat format;
|
||||
|
||||
keywordFormat.setForeground(Qt::blue);
|
||||
// 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);
|
||||
|
||||
QStringList keywordPatterns;
|
||||
keywordPatterns << "\\bif\\b" << "\\belse\\b" << "\\breturn\\b"<< "\\bimport\\b" << "\\bsignal\\b" << "\\bproperty\\b";
|
||||
// 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 = keywordFormat;
|
||||
rule.format = format;
|
||||
highlightingRules.append(rule);
|
||||
}
|
||||
|
||||
propertyFormat.setForeground(Qt::darkRed);
|
||||
rule.pattern = QRegExp("[A-z]+:");
|
||||
rule.format = propertyFormat;
|
||||
highlightingRules.append(rule);
|
||||
|
||||
lookupFormat.setForeground(Qt::magenta);
|
||||
//lookupFormat.setBackground(Qt::black);
|
||||
rule.pattern = QRegExp("\\b[0-9]+\\b");
|
||||
rule.format = lookupFormat;
|
||||
highlightingRules.append(rule);
|
||||
|
||||
quotationFormat.setForeground(Qt::darkGreen);
|
||||
// String literals
|
||||
format.setForeground(dark ? QColor("#e64ad7") : Qt::darkGreen);
|
||||
rule.format = format;
|
||||
rule.pattern = QRegExp("\".*\"");
|
||||
rule.format = quotationFormat;
|
||||
highlightingRules.append(rule);
|
||||
rule.pattern = QRegExp("'.*'");
|
||||
rule.format = quotationFormat;
|
||||
highlightingRules.append(rule);
|
||||
|
||||
itemFormat.setForeground(QColor(Qt::red));
|
||||
//itemFormat.setFontWeight(QFont::Bold);
|
||||
rule.pattern = QRegExp("[A-Z][a-z]+ ");
|
||||
rule.format = itemFormat;
|
||||
// comments
|
||||
format.setForeground(dark ? Qt::cyan : Qt::darkGray);
|
||||
rule.format = format;
|
||||
rule.pattern = QRegExp("//.*$");
|
||||
highlightingRules.append(rule);
|
||||
|
||||
cppObjectFormat.setForeground(QColor(Qt::blue).lighter());
|
||||
cppObjectFormat.setFontItalic(true);
|
||||
rule.pattern = QRegExp("_[A-z]+");
|
||||
rule.format = cppObjectFormat;
|
||||
rule.pattern = QRegExp("/*.*\\*/");
|
||||
highlightingRules.append(rule);
|
||||
}
|
||||
|
||||
@ -322,6 +165,9 @@ void ScriptSyntaxHighlighterPrivate::highlightBlock(const QString &text)
|
||||
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);
|
||||
}
|
||||
@ -339,4 +185,5 @@ void ScriptSyntaxHighlighterPrivate::highlightBlock(const QString &text)
|
||||
emit contentChanged(text);
|
||||
}
|
||||
|
||||
|
||||
#include "scriptsyntaxhighlighter.moc"
|
||||
|
||||
@ -4,163 +4,34 @@
|
||||
#include <QObject>
|
||||
#include <QSyntaxHighlighter>
|
||||
#include <QQuickTextDocument>
|
||||
#include <QAbstractItemDelegate>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class ScriptSyntaxHighlighterPrivate;
|
||||
class CompletionModel;
|
||||
class CompletionProxyModel;
|
||||
class Engine;
|
||||
|
||||
class ScriptSyntaxHighlighter : 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(CompletionProxyModel* completionModel READ completionModel CONSTANT)
|
||||
Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY cursorPositionChanged)
|
||||
Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
|
||||
public:
|
||||
explicit ScriptSyntaxHighlighter(QObject *parent = nullptr);
|
||||
|
||||
Engine* engine() const;
|
||||
void setEngine(Engine* engine);
|
||||
explicit ScriptSyntaxHighlighter(QObject *parent = nullptr);
|
||||
|
||||
QQuickTextDocument* document() const;
|
||||
void setDocument(QQuickTextDocument *document);
|
||||
|
||||
int cursorPosition() const;
|
||||
void setCursorPosition(int cursorPosition);
|
||||
|
||||
CompletionProxyModel* completionModel() const;
|
||||
|
||||
public slots:
|
||||
void complete(int index);
|
||||
void newLine();
|
||||
void indent(int from, int to);
|
||||
void unindent(int from, int to);
|
||||
void closeBlock();
|
||||
QColor backgroundColor() const;
|
||||
void setBackgroundColor(const QColor &backgroundColor);
|
||||
|
||||
signals:
|
||||
void documentChanged();
|
||||
void engineChanged();
|
||||
void cursorPositionChanged();
|
||||
|
||||
private slots:
|
||||
void onCursorPositionChanged(const QTextCursor &cursor);
|
||||
void backgroundColorChanged();
|
||||
|
||||
private:
|
||||
ScriptSyntaxHighlighterPrivate *m_highlighter = nullptr;
|
||||
QQuickTextDocument* m_document = nullptr;
|
||||
CompletionModel* m_completionModel = nullptr;
|
||||
CompletionProxyModel* m_proxyModel = nullptr;
|
||||
Engine *m_engine = nullptr;
|
||||
QTextCursor m_currentCursor;
|
||||
|
||||
QHash<QString, QStringList> m_classes;
|
||||
};
|
||||
|
||||
|
||||
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, bool addTrailingQuote = false, bool addComment = false)
|
||||
: text(text), displayText(displayText), addTrailingQuote(addTrailingQuote), addComment(addComment) {}
|
||||
Entry(const QString &text): text(text), displayText(text) {}
|
||||
QString text;
|
||||
QString displayText;
|
||||
bool addTrailingQuote = false;
|
||||
bool addComment = false;
|
||||
};
|
||||
CompletionModel(QObject *parent = nullptr): QAbstractListModel(parent) {}
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
|
||||
Q_UNUSED(parent)
|
||||
return m_list.count();
|
||||
}
|
||||
QHash<int, QByteArray> roleNames() const override {
|
||||
QHash<int, QByteArray> roles;
|
||||
roles.insert(Qt::UserRole, "text");
|
||||
roles.insert(Qt::DisplayRole, "displayText");
|
||||
return roles;
|
||||
}
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
|
||||
Q_UNUSED(role)
|
||||
switch (role) {
|
||||
case Qt::UserRole:
|
||||
return m_list.at(index.row()).text;
|
||||
case Qt::DisplayRole:
|
||||
return m_list.at(index.row()).displayText;
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
void clear() {
|
||||
beginResetModel();
|
||||
m_list.clear();
|
||||
endResetModel();
|
||||
emit countChanged();
|
||||
}
|
||||
void append(const Entry &entry) {
|
||||
beginInsertRows(QModelIndex(), m_list.count(), m_list.count());
|
||||
m_list.append(entry);
|
||||
endInsertRows();
|
||||
emit countChanged();
|
||||
}
|
||||
Entry get(int index) {
|
||||
return m_list.at(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 WRITE setFilter NOTIFY filterChanged)
|
||||
public:
|
||||
CompletionProxyModel(CompletionModel *model, QObject *parent = nullptr): QSortFilterProxyModel(parent), m_model(model) {
|
||||
setSourceModel(model);
|
||||
connect(model, &CompletionModel::countChanged, this, &CompletionProxyModel::countChanged);
|
||||
sort(0);
|
||||
}
|
||||
|
||||
CompletionModel::Entry get(int index) {
|
||||
return m_model->get(mapToSource(this->index(index, 0)).row());
|
||||
}
|
||||
|
||||
QString filter() const {
|
||||
return m_filter;
|
||||
}
|
||||
void setFilter(const QString &filter) {
|
||||
if (m_filter != filter) {
|
||||
m_filter = filter;
|
||||
emit filterChanged();
|
||||
invalidateFilter();
|
||||
emit countChanged();
|
||||
}
|
||||
}
|
||||
protected:
|
||||
bool filterAcceptsRow(int source_row, const QModelIndex &/*source_parent*/) const override {
|
||||
if (!m_filter.isEmpty()) {
|
||||
CompletionModel::Entry entry = m_model->get(source_row);
|
||||
if (!entry.displayText.startsWith(m_filter) && !entry.text.startsWith(m_filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
signals:
|
||||
void filterChanged();
|
||||
void countChanged();
|
||||
private:
|
||||
CompletionModel *m_model = nullptr;
|
||||
QString m_filter;
|
||||
QColor m_backgroundColor;
|
||||
};
|
||||
|
||||
#endif // SCRIPTSYNTAXHIGHLIGHTER_H
|
||||
|
||||
@ -50,3 +50,21 @@ void Scripts::addScript(Script *script)
|
||||
endInsertRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -24,6 +24,10 @@ public:
|
||||
|
||||
void clear();
|
||||
void addScript(Script *script);
|
||||
|
||||
Q_INVOKABLE Script *get(int index) const;
|
||||
Q_INVOKABLE Script *getScript(const QUuid &scriptId);
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
|
||||
@ -213,5 +213,6 @@
|
||||
<file>ui/images/browser/MediaBrowserIconSoundCloud.svg</file>
|
||||
<file>ui/images/browser/MediaBrowserIconDeezer.svg</file>
|
||||
<file>ui/images/view-grid-symbolic.svg</file>
|
||||
<file>ui/images/script.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@ -205,5 +205,8 @@
|
||||
<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>
|
||||
</RCC>
|
||||
|
||||
@ -11,7 +11,7 @@ Page {
|
||||
onBackPressed: pageStack.pop()
|
||||
|
||||
HeaderButton {
|
||||
imageSource: Qt.resolvedUrl("images/magic.svg")
|
||||
imageSource: Qt.resolvedUrl("images/script.svg")
|
||||
onClicked: {
|
||||
pageStack.push("magic/ScriptsPage.qml")
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ ApplicationWindow {
|
||||
property int largeFont: 20
|
||||
property int iconSize: 30
|
||||
property int delegateHeight: 60
|
||||
property color backgroundColor: Material.background
|
||||
|
||||
readonly property bool landscape: app.width > app.height
|
||||
|
||||
|
||||
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 |
@ -1,23 +1,52 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import "../components"
|
||||
import Nymea 1.0
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Controls.Material 2.1
|
||||
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"
|
||||
d.callId = engine.scriptManager.addScript(scriptEdit.text);
|
||||
}
|
||||
}
|
||||
|
||||
header: NymeaHeader {
|
||||
text: qsTr("Script editor")
|
||||
onBackPressed: pageStack.pop()
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
HeaderButton {
|
||||
imageSource: "../images/tick.svg"
|
||||
imageSource: "../images/media-playback-start.svg"
|
||||
onClicked: {
|
||||
if (!d.scriptId) {
|
||||
d.callId = engine.scriptManager.addScript(scriptEdit.text)
|
||||
} else {
|
||||
print("editing script", d.scriptId)
|
||||
print("editing script", d.scriptId, scriptEdit.text)
|
||||
d.callId = engine.scriptManager.editScript(d.scriptId, scriptEdit.text)
|
||||
}
|
||||
}
|
||||
@ -28,10 +57,7 @@ Page {
|
||||
id: d
|
||||
property int callId
|
||||
property var scriptId
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
d.callId = engine.scriptManager.addScript(scriptEdit.text);
|
||||
property string oldContent
|
||||
}
|
||||
|
||||
Connections {
|
||||
@ -41,55 +67,101 @@ Page {
|
||||
if (scriptError == "ScriptErrorNoError") {
|
||||
d.scriptId = scriptId;
|
||||
}
|
||||
errorListView.model = errors
|
||||
errorModel.update(errors);
|
||||
}
|
||||
}
|
||||
onScriptEdited: {
|
||||
if (id == d.callId) {
|
||||
errorListView.model = errors
|
||||
errorModel.update(errors)
|
||||
}
|
||||
}
|
||||
onScriptFetched: {
|
||||
if (id == d.callId && scriptError == "ScriptErrorNoError") {
|
||||
scriptEdit.text = content;
|
||||
d.oldContent = content;
|
||||
}
|
||||
}
|
||||
onScriptMessage: {
|
||||
if (scriptId !== d.scriptId) {
|
||||
return;
|
||||
}
|
||||
messagesModel.append({type: type, message: message})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this a SplitView when we can use Qt 5.13
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
color: "white"
|
||||
Layout.fillWidth: true
|
||||
Flickable {
|
||||
id: scriptFlickable
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
TextEdit {
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOn }
|
||||
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOn }
|
||||
|
||||
LineNumbers {
|
||||
id: lineNumbers
|
||||
}
|
||||
|
||||
TextArea.flickable: TextArea {
|
||||
id: scriptEdit
|
||||
anchors.fill: parent
|
||||
leftPadding: lineNumbers.width + 2
|
||||
rightPadding: 20
|
||||
bottomPadding: 28
|
||||
|
||||
font.family: "Monospace"
|
||||
font.pixelSize: app.extraSmallFont
|
||||
selectByMouse: true
|
||||
selectByKeyboard: true
|
||||
|
||||
onCursorPositionChanged: {
|
||||
if (completionBox.visible) {
|
||||
completion.update();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
print("key", event.key)
|
||||
// Things only to happen when we're not autocompleting
|
||||
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:
|
||||
syntax.newLine();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Tab:
|
||||
syntax.indent(selectionStart, selectionEnd);
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Backtab:
|
||||
syntax.unindent(selectionStart, selectionEnd);
|
||||
completion.newLine();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Space:
|
||||
if (!completionBox.visible && (event.modifiers & Qt.ControlModifier)) {
|
||||
completion.update();
|
||||
completionBox.show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// things to happen in any case
|
||||
switch (event.key) {
|
||||
case Qt.Key_BraceLeft:
|
||||
completion.insertAfterCursor("}");
|
||||
return;
|
||||
|
||||
case Qt.Key_BraceRight:
|
||||
syntax.closeBlock();
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
// Things to do only when we're autocompleting
|
||||
@ -109,82 +181,122 @@ Page {
|
||||
break;
|
||||
case Qt.Key_Enter:
|
||||
case Qt.Key_Return:
|
||||
syntax.complete(completionBox.currentIndex)
|
||||
completion.complete(completionBox.currentIndex)
|
||||
completionBox.hide();
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
CompletionBox {
|
||||
id: completionBox
|
||||
border.width: 1
|
||||
border.color: "black"
|
||||
height: syntax.completionModel.count * 30
|
||||
width: 200
|
||||
x: scriptEdit.cursorRectangle.x
|
||||
y: scriptEdit.cursorRectangle.y + scriptEdit.cursorRectangle.height
|
||||
visible: syntax.completionModel.count > 0 && !hidden
|
||||
property bool hidden: false
|
||||
Connections {
|
||||
target: syntax.completionModel
|
||||
onCountChanged: {
|
||||
completionBox.hidden = false;
|
||||
completionBox.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
property int currentIndex: 0
|
||||
function next() { currentIndex = (currentIndex + 1) % syntax.completionModel.count}
|
||||
function previous() {
|
||||
currentIndex--;
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = syntax.completionModel.count - 1
|
||||
}
|
||||
}
|
||||
function hide() {
|
||||
hidden = true;
|
||||
}
|
||||
|
||||
ListView {
|
||||
anchors.fill: parent
|
||||
model: syntax.completionModel
|
||||
delegate: Rectangle {
|
||||
height: 30
|
||||
width: parent.width
|
||||
color: index == completionBox.currentIndex ? "blue" : "white"
|
||||
Label {
|
||||
text: model.displayText
|
||||
color: "black"
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
syntax.complete(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
model: completion.model
|
||||
textArea: scriptEdit
|
||||
onComplete: {
|
||||
completion.complete(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: errorListView
|
||||
EditorPane {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 100
|
||||
delegate: Label {
|
||||
width: parent.width
|
||||
text: modelData
|
||||
Layout.preferredHeight: Math.min(implicitHeight, root.height / 4)
|
||||
|
||||
ScrollView {
|
||||
id: errorsPane
|
||||
anchors { fill: parent; margins: app.margins / 2 }
|
||||
property string title: qsTr("Errors")
|
||||
signal raise()
|
||||
|
||||
ListView {
|
||||
id: errorListView
|
||||
model: ListModel {
|
||||
id: errorModel
|
||||
property var errorLines: []
|
||||
function update(errors) {
|
||||
clear();
|
||||
var newErrorLines = []
|
||||
errors.forEach( function(error) {
|
||||
var parts = error.split(":")
|
||||
append({line: parseInt(parts[0]), column: parseInt(parts[1]), message: parts[2].trim()})
|
||||
newErrorLines.push(parseInt(parts[0]));
|
||||
})
|
||||
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.pixelSize: app.extraSmallFont
|
||||
font.family: "Monospace"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: consolePane
|
||||
anchors {fill: parent; margins: app.margins/ 2 }
|
||||
property string title: qsTr("Console")
|
||||
signal raise()
|
||||
|
||||
ListView {
|
||||
id: messagesListView
|
||||
model: ListModel {
|
||||
id: messagesModel
|
||||
onCountChanged: {
|
||||
if (count > 0) {
|
||||
consolePane.raise();
|
||||
}
|
||||
}
|
||||
}
|
||||
property bool autoScroll: true
|
||||
onCountChanged: {
|
||||
if (autoScroll) {
|
||||
messagesListView.positionViewAtEnd()
|
||||
}
|
||||
}
|
||||
onMovementEnded: {
|
||||
autoScroll = messagesListView.atYEnd;
|
||||
}
|
||||
|
||||
delegate: Label {
|
||||
width: parent.width
|
||||
text: model.message
|
||||
font.pixelSize: app.extraSmallFont
|
||||
font.family: "Monospace"
|
||||
color: model.type === "ScriptMessageTypeWarning" ? "red" : app.foregroundColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ScriptSyntaxHighlighter {
|
||||
id: syntax
|
||||
document: scriptEdit.textDocument
|
||||
backgroundColor: app.backgroundColor
|
||||
}
|
||||
|
||||
CodeCompletion {
|
||||
id: completion
|
||||
engine: _engine
|
||||
document: scriptEdit.textDocument
|
||||
cursorPosition: scriptEdit.cursorPosition
|
||||
|
||||
@ -39,6 +39,10 @@ Page {
|
||||
text: model.name
|
||||
subText: model.id
|
||||
canDelete: true
|
||||
onClicked: {
|
||||
pageStack.push("ScriptEditor.qml", {scriptId: model.id});
|
||||
}
|
||||
|
||||
onDeleteClicked: {
|
||||
print("removing script", model.id)
|
||||
d.pendingAction = engine.scriptManager.removeScript(model.id);
|
||||
|
||||
106
nymea-app/ui/magic/scripting/CompletionBox.qml
Normal file
106
nymea-app/ui/magic/scripting/CompletionBox.qml
Normal file
@ -0,0 +1,106 @@
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 2.2
|
||||
import Nymea 1.0
|
||||
|
||||
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
|
||||
x: textArea.cursorRectangle.x
|
||||
y: textArea.cursorRectangle.y + textArea.cursorRectangle.height
|
||||
|
||||
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() {
|
||||
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 + 4
|
||||
|
||||
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"
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors { left: parent.left; right: parent.right; margins: 4}
|
||||
text: model.displayText
|
||||
color: app.foregroundColor
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
font: root.font
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.complete(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
nymea-app/ui/magic/scripting/EditorPane.qml
Normal file
108
nymea-app/ui/magic/scripting/EditorPane.qml
Normal file
@ -0,0 +1,108 @@
|
||||
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: pane.shown ? "../images/down.svg" : "../images/up.svg"
|
||||
Layout.preferredHeight: app.iconSize / 2
|
||||
Layout.preferredWidth: height
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
57
nymea-app/ui/magic/scripting/LineNumbers.qml
Normal file
57
nymea-app/ui/magic/scripting/LineNumbers.qml
Normal file
@ -0,0 +1,57 @@
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Rectangle {
|
||||
id: lineNumbers
|
||||
width: {
|
||||
var ret = 10;
|
||||
var tmp = scriptEdit.lineCount
|
||||
while (tmp >= 10) {
|
||||
ret += 10;
|
||||
tmp /= 10;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
height: scriptEdit.height - 10
|
||||
color: (app.backgroundColor.r * 0.2126 + app.backgroundColor.g * 0.7152 + app.backgroundColor.b * 0.0722) * 255 < 128 ? "#202020" : "#e0e0e0"
|
||||
anchors { left: parent.left; leftMargin: scriptFlickable.contentX }
|
||||
Component.onCompleted: {
|
||||
print("..", app.backgroundColor.r)
|
||||
print("*** background", (app.backgroundColor.r * 0.2126 + app.backgroundColor.g * 0.7152 + app.backgroundColor.b * 0.0722) * 255 < 128 )
|
||||
}
|
||||
|
||||
Column {
|
||||
id: lineNumbersColumn
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 8
|
||||
Repeater {
|
||||
model: scriptEdit.lineCount
|
||||
delegate: Rectangle {
|
||||
id: lineNumberDelegate
|
||||
width: parent.width
|
||||
height: scriptEdit.contentHeight / scriptEdit.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: scriptEdit.font.pixelSize
|
||||
font.family: scriptEdit.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