// Copyright (C) 2004-2025 Robert Griebl // SPDX-License-Identifier: GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 11, 0) # include # include #else # include #endif #include "utility/exception.h" #include "script.h" #include "scriptmanager.h" Q_LOGGING_CATEGORY(LogScript, "script") class QmlException : public Exception { public: QmlException(const QList &errors, const char *msg) : Exception(msg) { for (auto &error : errors) m_errorString = m_errorString + u'\n' + error.toString(); } }; static QString stringifyType(QMetaType mt) { if (mt.metaObject()) { int qei = mt.metaObject()->indexOfClassInfo("QML.Element"); if (qei >= 0) { auto name = QString::fromLatin1(mt.metaObject()->classInfo(qei).value()); if (name == u"auto") return QString::fromLatin1(mt.metaObject()->className()).section(u"::"_qs, -1); else return name; } } switch (mt.id()) { case QMetaType::QString : return u"string"_qs; case QMetaType::QStringList : return u"list"_qs; case QMetaType::QVariant : return u"var"_qs; case QMetaType::QVariantList: return u"list"_qs; case QMetaType::QVariantMap : return u"object"_qs; case QMetaType::QDate : case QMetaType::QTime : case QMetaType::QDateTime : return u"date"_qs; case QMetaType::QSize : case QMetaType::QSizeF : return u"size"_qs; case QMetaType::QColor : return u"color"_qs; case QMetaType::Float : case QMetaType::Double : return u"real"_qs; default : return QString::fromLatin1(mt.name()); } } static QString stringify(const QVariant &value, int level, bool indentFirstLine, QStack> &recursionGuard); static QString stringifyQObject(const QObject *o, const QMetaObject *mo, int level, bool indentFirstLine, QStack> &recursionGuard) { QString str; QString indent = QString(level * 2, u' '); QString nextIndent = QString((level + 1) * 2, u' '); if (indentFirstLine) str.append(indent); const auto guardKey = std::make_pair(o , mo); if (recursionGuard.contains(guardKey)) { str.append(u""); return str; } recursionGuard.push(guardKey); str = str + stringifyType(mo->metaType()) + u" {\n"; QByteArrayList noStringify; if (int nostr = mo->indexOfClassInfo("bsNoStringify"); nostr >= 0) noStringify = QByteArray(mo->classInfo(nostr).value()).split(','); auto stringifyProperties = [&](const QMetaObject *mo) { bool isGadget = mo->metaType().flags().testFlag(QMetaType::IsGadget); qsizetype count = 0; for (int i = mo->propertyOffset(); i < mo->propertyCount(); ++i) { QMetaProperty p = mo->property(i); QString valueStr; if (noStringify.contains(p.name())) valueStr = u"<...>"_qs; else valueStr = stringify(isGadget ? p.readOnGadget(o) : p.read(o), level + 1, false, recursionGuard); str = str + nextIndent + stringifyType(p.metaType()) + u' ' + QString::fromLatin1(p.name()) + u": " + valueStr + u'\n'; ++count; } return count; }; QList superMos; for (auto *smo = mo; smo; smo = smo->superClass()) { if ((smo != &QObject::staticMetaObject) || !o->objectName().isEmpty()) superMos.prepend(smo); if (smo->metaType().flags().testFlag(QMetaType::IsGadget)) break; } int propCount = 0; for (const auto *smo : superMos) propCount += stringifyProperties(smo); /* for (int i = mo->methodOffset(); i < mo->methodCount(); ++i) { QMetaMethod m = mo->method(i); switch (m.methodType()) { case QMetaMethod::Slot: break; case QMetaMethod::Method: break; default: continue; } const auto pnames = m.parameterNames(); QStringList params; for (int pi = 0; pi < m.parameterCount(); ++pi) params.append(stringifyType(m.parameterMetaType(pi)) + u' ' + QLatin1String(pnames.at(pi))); str = str + nextIndent + stringifyType(m.returnMetaType()) + u' ' + QLatin1String(m.name()) + u'(' + params.join(u", ") + u")\n"; } */ if (!propCount) str[str.length() - 1] = u'}'; else str = str + indent + u'}'; Q_ASSERT(recursionGuard.top() == guardKey); recursionGuard.pop(); return str; } static QString stringify(const QVariant &value, int level = 0, bool indentFirstLine = false) { QStack> recursionGuard; return stringify(value, level, indentFirstLine, recursionGuard); } static QString stringify(const QVariant &value, int level, bool indentFirstLine, QStack> &recursionGuard) { if (value.typeId() == qMetaTypeId()) return stringify(value.value().toVariant(), level, indentFirstLine, recursionGuard); if (value.typeId() == qMetaTypeId()) return stringify(value.value(), level, indentFirstLine, recursionGuard); QString str; QString indent = QString(level * 2, u' '); QString nextIndent = QString((level + 1) * 2, u' '); if (indentFirstLine) str.append(indent); if (value.canConvert()) { #if QT_VERSION < QT_VERSION_CHECK(6, 11, 0) auto hash = value.value(); #else auto hash = value.value(); #endif if (hash.size() == 0) { str.append(u"{}"); } else if (level > 0) { str = str + u"{ <" + QString::number(hash.size()) + u" mappings> }"; } else { str.append(u"{\n"); for (auto it = hash.constBegin(); it != hash.constEnd(); ++it) { str = str + nextIndent + it.key().toString() + u": " + stringify(it.value(), level + 1, false, recursionGuard); if ((it + 1) != hash.constEnd()) str.append(u','); str.append(u'\n'); } str = str + indent + u'}'; } } else if (value.canConvert() && (value.typeId() != QMetaType::QString)) { #if QT_VERSION < QT_VERSION_CHECK(6, 11, 0) auto list = value.value(); #else auto list = value.value(); #endif if (list.size() == 0) { str.append(u"[]"); } else if (level > 0) { str = str + u"[ <" + QString::number(list.size()) + u" items> ]"; } else { str = str.append(u"[\n"); for (qsizetype i = 0; i < list.size(); ++i) { str = str + stringify(list.at(i), level + 1, true, recursionGuard); if (i < (list.size() - 1)) str.append(u','); str.append(u'\n'); } str = str + indent + u']'; } } else { switch (int(value.typeId())) { case QMetaType::QString: { if (level > 0) str = str + u'"' + value.toString().remove(u"\0"_qs) + u'"'; else str = value.toString().remove(u"\0"_qs); break; } case QMetaType::QSize: case QMetaType::QSizeF: { const auto s = value.toSizeF(); str = QString::number(s.width()) + u" x " + QString::number(s.height()); break; } case QMetaType::QDateTime: { const auto dt = value.toDateTime(); str = dt.isValid() ? dt.toString() : u""_qs; break; } case QMetaType::QModelIndex: { const auto idx = value.toModelIndex(); str = u"index(r: " + QString::number(idx.row()) + u", c: " + QString::number(idx.column()) + u")"; break; } case QMetaType::QObjectStar: { auto *o = qvariant_cast(value); if (!o) { str.append(u""); break; } str.append(stringifyQObject(o, o->metaObject(), level, false, recursionGuard)); break; } case QMetaType::Nullptr: str = u"null"_qs; break; default: { QMetaType meta(value.typeId()); if (meta.flags().testFlag(QMetaType::IsGadget)) { if (value.data() && meta.metaObject()) str.append(stringifyQObject(reinterpret_cast(value.data()), meta.metaObject(), level, false, recursionGuard)); } else if (meta.flags().testFlag(QMetaType::PointerToQObject)) { auto *o = qvariant_cast(value); if (!o) { str.append(u""); break; } str.append(stringifyQObject(o, o->metaObject(), level, false, recursionGuard)); } else { str.append(value.toString()); } break; } } } return str; } ScriptManager::ScriptManager(QQmlEngine *engine) : m_engine(engine) { } ScriptManager::~ScriptManager() { clearScripts(); s_inst = nullptr; } ScriptManager *ScriptManager::s_inst = nullptr; ScriptManager *ScriptManager::inst() { return s_inst; } ScriptManager *ScriptManager::create(QQmlEngine *engine) { Q_ASSERT(!s_inst); s_inst = new ScriptManager(engine); return s_inst; } bool ScriptManager::reload() { emit aboutToReload(); clearScripts(); QStringList spath = { u":/extensions"_qs }; QString dataloc = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (!dataloc.isEmpty()) spath.prepend(dataloc + u"/extensions"_qs); for (const QString &path : std::as_const(spath)) { QDir dir(path); if (!path.startsWith(u':')) { qCInfo(LogScript) << "Loading scripts from directory:" << path; if (!dir.exists()) dir.mkpath(u"."_qs); } const QFileInfoList fis = dir.entryInfoList(QStringList(u"*.bs.qml"_qs), QDir::Files | QDir::Readable); for (const QFileInfo &fi : fis) { QString filePath = fi.absoluteFilePath(); if (filePath.startsWith(u':')) filePath = u"qrc://" + filePath.mid(1); try { loadScript(filePath); qCInfo(LogScript).noquote() << " [ ok ]" << fi.fileName(); } catch (const Exception &e) { qCWarning(LogScript).noquote() << " [fail]" << fi.fileName() << ":" << e.what(); } } } emit reloaded(); return !m_scripts.isEmpty(); } void ScriptManager::loadScript(const QString &fileName) { Q_ASSERT(m_engine); auto comp = new QQmlComponent(m_engine, fileName, m_engine); auto ctx = new QQmlContext(m_engine, m_engine); std::unique_ptr root { comp->create(ctx) }; if (!root) throw QmlException(comp->errors(), "Could not load QML file %1").arg(fileName); if (root && !qobject_cast