// // JSAPIExample.cpp // plugins/JSAPIExample/src // // Copyright (c) 2019-2020 humbletim (humbletim@gmail.com) // Copyright (c) 2019 Kalila L. (kasenvr@gmail.com) // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // Example of prototyping new JS APIs by leveraging the existing plugin system. #include #include #include #include #include #include #include #include #include #include #include // for ::settingsFilename() #include // for usecTimestampNow() #include // NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes) namespace REPLACE_ME_WITH_UNIQUE_NAME { static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1"; static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample"; QLoggingCategory logger{ "jsapiexample" }; inline QVariant raiseScriptingError(QScriptContext* context, const QString& message, const QVariant& returnValue = QVariant()) { if (context) { // when a QScriptContext is available throw an actual JS Exception (which can be caught using try/catch on JS side) context->throwError(message); } else { // otherwise just log the error qCWarning(logger) << "error:" << message; } return returnValue; } QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error); class JSAPIExample : public QObject, public QScriptable { Q_OBJECT Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json") Q_PROPERTY(QString version MEMBER _version CONSTANT) public: JSAPIExample() { setObjectName(JSAPI_EXPORT_NAME); auto scriptInit = DependencyManager::get(); if (!scriptInit) { qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this; return; } qCWarning(logger) << "registering w/ScriptInitializerMixin..." << scriptInit.data(); scriptInit->registerScriptInitializer([this](QScriptEngine* engine) { auto value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater); engine->globalObject().setProperty(objectName(), value); // qCDebug(logger) << "setGlobalInstance" << objectName() << engine->property("fileName"); }); // qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread(); } // NOTES: everything within the "public slots:" section below will be available from JS via overall plugin QObject // also, to demonstrate future-proofing JS API code, QVariant's are used throughout most of these examples -- // which still makes them very Qt-specific, but avoids depending directly on deprecated QtScript/QScriptValue APIs. // (as such this plugin class and its methods remain forward-compatible with other engines like QML's QJSEngine) public slots: // returns a pretty-printed representation for logging eg: print(JSAPIExample) inline QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); } /**jsdoc * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms * @example Measure current setTimeout accuracy. * var expected = 1000; * var start = JSAPIExample.now(); * Script.setTimeout(function () { * var elapsed = (JSAPIExample.now() - start)/1000; * print("expected (ms):", expected, "actual (ms):", elapsed); * }, expected); */ QVariant now() const { return usecTimestampNow(); } /**jsdoc * Example of returning a JS Object key-value map * @example "zip" a list of keys and corresponding values to form key-value map * print(JSON.stringify(JSAPIExample.zip(["a","b"], [1,2])); // { "a": 1, "b": 2 } */ QVariant zip(const QStringList& keys, const QVariantList& values) const { QVariantMap out; for (int i = 0; i < keys.size(); i++) { out[keys[i]] = i < values.size() ? values[i] : QVariant(); } return out; } /**jsdoc * Example of returning a JS Array result * @example emulate Object.values(keyValues) * print(JSON.stringify(JSAPIExample.values({ "a": 1, "b": 2 }))); // [1,2] */ QVariant values(const QVariantMap& keyValues) const { return keyValues.values(); } /**jsdoc * Another example of returning JS Array data * @example generate an integer sequence (inclusive of [from, to]) * print(JSON.stringify(JSAPIExample.seq(1,5)));// [1,2,3,4,5] */ QVariant seq(int from, int to) const { QVariantList out; for (int i = from; i <= to; i++) { out.append(i); } return out; } /**jsdoc * Example of returning arbitrary binary data from C++ (resulting in a JS ArrayBuffer) * see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#Examples * @example return compressed/decompressed versions of the input data * var data = "testing 1 2 3"; * var z = JSAPIExample.qCompressString(data); // z will be an ArrayBuffer * var u = JSAPIExample.qUncompressString(z); // u will be a String value * print(JSON.stringify({ input: data, compressed: z.byteLength, output: u, uncompressed: u.length })); */ QVariant qCompressString(const QString& data, int compress_level = -1) const { return qCompress(data.toUtf8(), compress_level); } QString qUncompressString(const QByteArray& data) const { return QString::fromUtf8(qUncompress(data)); } /** * Example of exposing a custom "managed" C++ QObject to JS * The lifecycle of the created QObject* instance becomes managed by the invoking QScriptEngine -- * it will be automatically cleaned up once no longer reachable from any JS variables/closures. * @example access persistent settings stored in separate .json files * var settings = JSAPIExample.getScopedSettings("example"); * print("example settings stored in:", settings.fileName()); * print("(before) example::timestamp", settings.value("timestamp")); * settings.setValue("timestamp", Date.now()); * print("(after) example::timestamp", settings.value("timestamp")); * print("all example::* keys", settings.allKeys()); * settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector */ QScriptValue getScopedSettings(const QString& scope) { auto engine = QScriptable::engine(); if (!engine) return QScriptValue::NullValue; QString error; auto cppValue = createScopedSettings(scope, engine, error); if (!cppValue) { raiseScriptingError(context(), "error creating scoped settings instance: " + error); return QScriptValue::NullValue; } return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater); } private: const QString _version{ JSAPI_SEMANTIC_VERSION }; }; // Example of how to create a QObject class that can have multiple instances created from the JS side // JSSettingsHelper emulates a subset of QSetting APIs: // fileName() -- full path to the scoped settings .json file // allKeys() -- all previously stored keys available in the scoped settings file // value(key, defaultValue) -- retrieve a stored value // setValue(key, newValue) -- set/update a stored value class JSSettingsHelper : public QObject { Q_OBJECT QString _scope; QString _fileName; QSharedPointer _settings; public: operator bool() const { return (bool)_settings; } JSSettingsHelper(const QString& scope, QObject* parent = nullptr) : QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)), _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {} ~JSSettingsHelper() { qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this; } public slots: QString fileName() const { return _settings ? _settings->fileName() : ""; } QString toString() const { return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings); } QVariant value(const QString& key, const QVariant& defaultValue = QVariant()) { return _settings ? _settings->value(key, defaultValue) : defaultValue; } bool setValue(const QString& key, const QVariant& value) { if (_settings) { _settings->setValue(key, value); return true; } return false; } QStringList allKeys() const { return _settings ? _settings->allKeys() : QStringList{}; } protected: QString getLocalSettingsPath(const QString& scope) const { // generate a prefixed filename (relative to the main application's Interface.json file) const QString fileName = QString("jsapi_%1.json").arg(scope); return QFileInfo(::settingsFilename()).dir().filePath(fileName); } }; QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error) { const QRegExp VALID_SETTINGS_SCOPE{ "[-_A-Za-z0-9]{1,64}" }; if (!VALID_SETTINGS_SCOPE.exactMatch(scope)) { error = QString("invalid scope (expected alphanumeric <= 64 chars not '%1')").arg(scope); return nullptr; } return new JSSettingsHelper(scope, parent); } } // namespace REPLACE_ME_WITH_UNIQUE_NAME #include "JSAPIExample.moc"