From fa3a886ce22e1259f56fc86a149c9dc65f7100ed Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 20:48:59 -0400 Subject: [PATCH 1/5] rename plugin folder --- .../CMakeLists.txt | 0 .../src/JSAPIExample.cpp} | 0 .../src/plugin.json | 0 .../KasenAPIExample/src/ExampleScriptPlugin.h | 58 ------------------- 4 files changed, 58 deletions(-) rename plugins/{KasenAPIExample => JSAPIExample}/CMakeLists.txt (100%) rename plugins/{KasenAPIExample/src/KasenAPIExample.cpp => JSAPIExample/src/JSAPIExample.cpp} (100%) rename plugins/{KasenAPIExample => JSAPIExample}/src/plugin.json (100%) delete mode 100644 plugins/KasenAPIExample/src/ExampleScriptPlugin.h diff --git a/plugins/KasenAPIExample/CMakeLists.txt b/plugins/JSAPIExample/CMakeLists.txt similarity index 100% rename from plugins/KasenAPIExample/CMakeLists.txt rename to plugins/JSAPIExample/CMakeLists.txt diff --git a/plugins/KasenAPIExample/src/KasenAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp similarity index 100% rename from plugins/KasenAPIExample/src/KasenAPIExample.cpp rename to plugins/JSAPIExample/src/JSAPIExample.cpp diff --git a/plugins/KasenAPIExample/src/plugin.json b/plugins/JSAPIExample/src/plugin.json similarity index 100% rename from plugins/KasenAPIExample/src/plugin.json rename to plugins/JSAPIExample/src/plugin.json diff --git a/plugins/KasenAPIExample/src/ExampleScriptPlugin.h b/plugins/KasenAPIExample/src/ExampleScriptPlugin.h deleted file mode 100644 index 76c0a494d7..0000000000 --- a/plugins/KasenAPIExample/src/ExampleScriptPlugin.h +++ /dev/null @@ -1,58 +0,0 @@ -// -// ExampleScriptPlugin.h -// plugins/KasenAPIExample/src -// -// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com -// Copyright 2019 Kasen IO -// -// Authored by: Humbletim (humbletim@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 -// -// Supporting file containing all QtScript specific integration. - -#ifndef EXAMPLE_SCRIPT_PLUGIN_H -#define EXAMPLE_SCRIPT_PLUGIN_H - -#if DEV_BUILD -#pragma message("QtScript is deprecated see: doc.qt.io/qt-5/topics-scripting.html") -#endif -#include - -#include -#include -#include - -namespace example { - -extern const QLoggingCategory& logger; - -inline void setGlobalInstance(QScriptEngine* engine, const QString& name, QObject* object) { - auto value = engine->newQObject(object, QScriptEngine::QtOwnership); - engine->globalObject().setProperty(name, value); - qCDebug(logger) << "setGlobalInstance" << name << engine->property("fileName"); -} - -class ScriptPlugin : public QObject { - Q_OBJECT - QString _version; - Q_PROPERTY(QString version MEMBER _version CONSTANT) -protected: - inline ScriptPlugin(const QString& name, const QString& version) : _version(version) { - setObjectName(name); - if (!DependencyManager::get()) { - qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this; - return; - } - qCWarning(logger) << "registering w/ScriptInitializerMixin..." << DependencyManager::get().data(); - DependencyManager::get()->registerScriptInitializer( - [this](QScriptEngine* engine) { setGlobalInstance(engine, objectName(), this); }); - } -public slots: - inline QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); } -}; - -} // namespace example - -#endif \ No newline at end of file From f54b1c5fed48aa87a1ed16bd3d13affb5eb2837d Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 21:26:01 -0400 Subject: [PATCH 2/5] revamped modkit plugin example --- .../qml/hifi/dialogs/security/Security.qml | 4 +- plugins/CMakeLists.txt | 2 +- plugins/JSAPIExample/CMakeLists.txt | 4 +- plugins/JSAPIExample/src/JSAPIExample.cpp | 261 ++++++++++++------ plugins/JSAPIExample/src/plugin.json | 21 +- 5 files changed, 176 insertions(+), 116 deletions(-) diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index b1f62633e7..cfa420955b 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -312,9 +312,9 @@ Rectangle { parent.color = hifi.colors.blueHighlight; } onClicked: { - lightboxPopup.titleText = "Script Plugin Infrastructure by Kasen"; + lightboxPopup.titleText = "Script Plugin Infrastructure"; lightboxPopup.bodyText = "Toggles the activation of scripting plugins in the 'plugins/scripting' folder. \n\n" - + "Created by https://kasen.io/"; + + "Created by:\n humbletim@gmail.com\n kasenvr@gmail.com"; lightboxPopup.button1text = "OK"; lightboxPopup.button1method = function() { lightboxPopup.visible = false; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1448e14c72..a72371f544 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -53,5 +53,5 @@ set(DIR "opusCodec") add_subdirectory(${DIR}) # example plugins -set(DIR "KasenAPIExample") +set(DIR "JSAPIExample") add_subdirectory(${DIR}) diff --git a/plugins/JSAPIExample/CMakeLists.txt b/plugins/JSAPIExample/CMakeLists.txt index 96ac84e10d..a8fa0a1fd6 100644 --- a/plugins/JSAPIExample/CMakeLists.txt +++ b/plugins/JSAPIExample/CMakeLists.txt @@ -1,3 +1,3 @@ -set(TARGET_NAME KasenAPIExample) +set(TARGET_NAME JSAPIExample) setup_hifi_client_server_plugin(scripting) -link_hifi_libraries(shared plugins avatars networking graphics gpu) +link_hifi_libraries(shared plugins) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index 720c47f6cd..1ac8d56fc5 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -1,141 +1,218 @@ // -// KasenAPIExample.cpp -// plugins/KasenAPIExample/src +// JSAPIExample.cpp +// plugins/JSAPIExample/src // -// Created by Kasen IO on 2019.07.14 | realities.dev | kasenvr@gmail.com -// Copyright 2019 Kasen IO -// -// Authored by: Humbletim (humbletim@gmail.com) +// 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 "ExampleScriptPlugin.h" - #include +#include +#include +#include #include #include #include #include +#include +#include -#include -#include +#include // for ::settingsFilename() +#include // for usecTimestampNow() +#include -namespace custom_api_example { +// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes) +namespace REPLACE_ME_WITH_UNIQUE_NAME { -QLoggingCategory logger{ "custom_api_example" }; +static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1"; +static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample"; -class KasenAPIExample : public example::ScriptPlugin { +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 "KasenAPIExample" FILE "plugin.json") + Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json") + Q_PROPERTY(QString version MEMBER _version CONSTANT) public: - KasenAPIExample() : example::ScriptPlugin("KasenAPIExample", "0.0.1") { - qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread(); + 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 = KasenAPIExample.now(); + * var start = JSAPIExample.now(); * Script.setTimeout(function () { - * var elapsed = (KasenAPIExample.now() - start)/1000; + * var elapsed = (JSAPIExample.now() - start)/1000; * print("expected (ms):", expected, "actual (ms):", elapsed); * }, expected); */ - QVariant now() const { - return usecTimestampNow(); - } + QVariant now() const { return usecTimestampNow(); } /**jsdoc - * Returns the available blendshape names for an avatar. - * @example Get blendshape names - * print(JSON.stringify(KasenAPIExample.getBlendshapeNames(MyAvatar.sessionUUID))); + * 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 } */ - QStringList getBlendshapeNames(const QUuid& avatarID) const { - QVector out; - if (auto head = getAvatarHead(avatarID)) { - for (const auto& kv : head->getBlendshapeMap().toStdMap()) { - if (kv.second >= out.size()) out.resize(kv.second+1); - out[kv.second] = kv.first; - } - } - return out.toList(); - } - - /**jsdoc - * Returns a key-value object with active (non-zero) blendshapes. - * eg: { JawOpen: 1.0, ... } - * @example Get active blendshape map - * print(JSON.stringify(KasenAPIExample.getActiveBlendshapes(MyAvatar.sessionUUID))); - */ - QVariant getActiveBlendshapes(const QUuid& avatarID) const { - if (auto head = getAvatarHead(avatarID)) { - return head->toJson()["blendShapes"].toVariant(); - } - return {}; - } - - QVariant getBlendshapeMapping(const QUuid& avatarID) const { + QVariant zip(const QStringList& keys, const QVariantList& values) const { QVariantMap out; - if (auto head = getAvatarHead(avatarID)) { - for (const auto& kv : head->getBlendshapeMap().toStdMap()) { - out[kv.first] = kv.second; - } + 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; } - QVariant getBlendshapes(const QUuid& avatarID) const { - QVariantMap result; - if (auto head = getAvatarHead(avatarID)) { - QStringList names = getBlendshapeNames(avatarID); - auto states = head->getBlendshapeStates(); - result = { - { "base", zipNonZeroValues(names, states.base) }, - { "summed", zipNonZeroValues(names, states.summed) }, - { "transient", zipNonZeroValues(names, states.transient) }, - }; + /**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 result; + return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater); } private: - static QVariantMap zipNonZeroValues(const QStringList& keys, const QVector& values) { - QVariantMap out; - for (int i=1; i < values.size(); i++) { - if (fabs(values[i]) > 1.0e-6f) { - out[keys.value(i)] = values[i]; - } - } - return out; + 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; } - struct _HeadHelper : public HeadData { - QMap getBlendshapeMap() const { - return BLENDSHAPE_LOOKUP_MAP; + bool setValue(const QString& key, const QVariant& value) { + if (_settings) { + _settings->setValue(key, value); + return true; } - struct States { QVector base, summed, transient; }; - States getBlendshapeStates() const { - return { - _blendshapeCoefficients, - _summedBlendshapeCoefficients, - _transientBlendshapeCoefficients - }; - } - }; - static const _HeadHelper* getAvatarHead(const QUuid& avatarID) { - auto avatars = DependencyManager::get(); - auto avatar = avatars ? avatars->getAvatarBySessionID(avatarID) : nullptr; - auto head = avatar ? avatar->getHeadData() : nullptr; - return reinterpret_cast(head); + 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); } -const QLoggingCategory& example::logger{ custom_api_example::logger }; +} // namespace REPLACE_ME_WITH_UNIQUE_NAME -#include "KasenAPIExample.moc" +#include "JSAPIExample.moc" diff --git a/plugins/JSAPIExample/src/plugin.json b/plugins/JSAPIExample/src/plugin.json index 3e6931deec..f28c7fb988 100644 --- a/plugins/JSAPIExample/src/plugin.json +++ b/plugins/JSAPIExample/src/plugin.json @@ -1,21 +1,4 @@ { - "name":"Kasen JS API Example", - "version": 1, - "package": { - "author": "Revofire", - "homepage": "www.realities.dev", - "version": "0.0.1", - "engines": { - "hifi-interface": ">= 0.83.0", - "hifi-assignment-client": ">= 0.83.0" - }, - "config": { - "client": true, - "entity_client": true, - "entity_server": true, - "edit_filter": true, - "agent": true, - "avatar": true - } - } + "name":"JS API Example", + "version": 1 } From 7fb0173ef7df5b6f00637104742052fc9a1e8f64 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 21:58:32 -0400 Subject: [PATCH 3/5] clarifications per peer review --- plugins/JSAPIExample/src/JSAPIExample.cpp | 30 +++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index 1ac8d56fc5..f3947b2f4f 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -73,8 +73,9 @@ public: // (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); } + // pretty-printed representation for logging eg: print(JSAPIExample) + // (note: Qt script engines automatically look for a ".toString" method on native classes when coercing values to strings) + QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); } /**jsdoc * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms @@ -100,12 +101,16 @@ public slots: } 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(); } + QVariant values(const QVariantMap& keyValues) const { + QVariantList values = keyValues.values(); + return values; + } /**jsdoc * Another example of returning JS Array data @@ -129,10 +134,14 @@ public slots: * 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); + QVariant qCompressString(const QString& jsString, int compress_level = -1) const { + QByteArray arrayBuffer = qCompress(jsString.toUtf8(), compress_level); + return arrayBuffer; + } + QVariant qUncompressString(const QByteArray& arrayBuffer) const { + QString jsString = QString::fromUtf8(qUncompress(arrayBuffer)); + return jsString; } - QString qUncompressString(const QByteArray& data) const { return QString::fromUtf8(qUncompress(data)); } /** * Example of exposing a custom "managed" C++ QObject to JS @@ -149,12 +158,13 @@ public slots: */ QScriptValue getScopedSettings(const QString& scope) { auto engine = QScriptable::engine(); - if (!engine) return QScriptValue::NullValue; + 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; + raiseScriptingError(context(), "error creating scoped settings instance: " + error); + return QScriptValue::NullValue; } return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater); } @@ -163,7 +173,6 @@ 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 @@ -204,6 +213,7 @@ protected: } }; +// verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance 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)) { From dedf6a6975957097bb56d344f8d5fa74f3662ee6 Mon Sep 17 00:00:00 2001 From: humbletim Date: Wed, 28 Oct 2020 22:24:29 -0400 Subject: [PATCH 4/5] further cleanup per peer review (thanks fluffy!) --- plugins/JSAPIExample/src/JSAPIExample.cpp | 94 ++++++++++++++--------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index f3947b2f4f..d0e5a27869 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -22,7 +22,7 @@ #include #include // for ::settingsFilename() -#include // for usecTimestampNow() +#include // for ::usecTimestampNow() #include // NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes) @@ -150,9 +150,9 @@ public slots: * @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")); + * print("(before) example::timestamp", settings.getValue("timestamp")); * settings.setValue("timestamp", Date.now()); - * print("(after) example::timestamp", settings.value("timestamp")); + * print("(after) example::timestamp", settings.getValue("timestamp")); * print("all example::* keys", settings.allKeys()); * settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector */ @@ -173,44 +173,24 @@ private: const QString _version{ JSAPI_SEMANTIC_VERSION }; }; -// 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 +// JSSettingsHelper wraps a scoped (prefixed/separate) QSettings and exposes a subset of QSetting APIs as slots class JSSettingsHelper : public QObject { Q_OBJECT +public: + JSSettingsHelper(const QString& scope, QObject* parent = nullptr); + ~JSSettingsHelper(); + operator bool() const; +public slots: + QString fileName() const; + QString toString() const; + QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant()); + bool setValue(const QString& key, const QVariant& value); + QStringList allKeys() const; +protected: 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); - } + QString getLocalSettingsPath(const QString& scope) const; }; // verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance @@ -223,6 +203,48 @@ QObject* createScopedSettings(const QString& scope, QObject* parent, QString& er return new JSSettingsHelper(scope, parent); } +// -------------------------------------------------- +// ----- inline JSSettingsHelper implementation ----- +JSSettingsHelper::operator bool() const { + return (bool)_settings; +} +JSSettingsHelper::JSSettingsHelper(const QString& scope, QObject* parent) : + QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)), + _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) { +} +JSSettingsHelper::~JSSettingsHelper() { + qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this; +} +QString JSSettingsHelper::fileName() const { + return _settings ? _settings->fileName() : ""; +} +QString JSSettingsHelper::toString() const { + return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings); +} +QVariant JSSettingsHelper::getValue(const QString& key, const QVariant& defaultValue) { + return _settings ? _settings->value(key, defaultValue) : defaultValue; +} +bool JSSettingsHelper::setValue(const QString& key, const QVariant& value) { + if (_settings) { + if (value.isValid()) { + _settings->setValue(key, value); + } else { + _settings->remove(key); + } + return true; + } + return false; +} +QStringList JSSettingsHelper::allKeys() const { + return _settings ? _settings->allKeys() : QStringList{}; +} +QString JSSettingsHelper::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); +} +// ----- /inline JSSettingsHelper implementation ----- + } // namespace REPLACE_ME_WITH_UNIQUE_NAME #include "JSAPIExample.moc" From 5c2a8bd45938bce257af5ed15027d8b11943da35 Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 31 Oct 2020 14:11:05 -0400 Subject: [PATCH 5/5] changes per CR --- plugins/JSAPIExample/src/JSAPIExample.cpp | 423 +++++++++++----------- 1 file changed, 212 insertions(+), 211 deletions(-) diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index d0e5a27869..ed637e198b 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -28,222 +28,223 @@ // 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"; + static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1"; + static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample"; -QLoggingCategory logger{ "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: - // pretty-printed representation for logging eg: print(JSAPIExample) - // (note: Qt script engines automatically look for a ".toString" method on native classes when coercing values to strings) - 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 { - QVariantList values = keyValues.values(); - return 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& jsString, int compress_level = -1) const { - QByteArray arrayBuffer = qCompress(jsString.toUtf8(), compress_level); - return arrayBuffer; - } - QVariant qUncompressString(const QByteArray& arrayBuffer) const { - QString jsString = QString::fromUtf8(qUncompress(arrayBuffer)); - return jsString; - } - - /** - * 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.getValue("timestamp")); - * settings.setValue("timestamp", Date.now()); - * print("(after) example::timestamp", settings.getValue("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 }; -}; - -// JSSettingsHelper wraps a scoped (prefixed/separate) QSettings and exposes a subset of QSetting APIs as slots -class JSSettingsHelper : public QObject { - Q_OBJECT -public: - JSSettingsHelper(const QString& scope, QObject* parent = nullptr); - ~JSSettingsHelper(); - operator bool() const; -public slots: - QString fileName() const; - QString toString() const; - QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant()); - bool setValue(const QString& key, const QVariant& value); - QStringList allKeys() const; -protected: - QString _scope; - QString _fileName; - QSharedPointer _settings; - QString getLocalSettingsPath(const QString& scope) const; -}; - -// verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance -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); -} - -// -------------------------------------------------- -// ----- inline JSSettingsHelper implementation ----- -JSSettingsHelper::operator bool() const { - return (bool)_settings; -} -JSSettingsHelper::JSSettingsHelper(const QString& scope, QObject* parent) : - QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)), - _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) { -} -JSSettingsHelper::~JSSettingsHelper() { - qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this; -} -QString JSSettingsHelper::fileName() const { - return _settings ? _settings->fileName() : ""; -} -QString JSSettingsHelper::toString() const { - return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings); -} -QVariant JSSettingsHelper::getValue(const QString& key, const QVariant& defaultValue) { - return _settings ? _settings->value(key, defaultValue) : defaultValue; -} -bool JSSettingsHelper::setValue(const QString& key, const QVariant& value) { - if (_settings) { - if (value.isValid()) { - _settings->setValue(key, value); + 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 { - _settings->remove(key); + // otherwise just log the error + qCWarning(logger) << "error:" << message; } - return true; + return returnValue; } - return false; -} -QStringList JSSettingsHelper::allKeys() const { - return _settings ? _settings->allKeys() : QStringList{}; -} -QString JSSettingsHelper::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); -} -// ----- /inline JSSettingsHelper implementation ----- + + 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: + // pretty-printed representation for logging eg: print(JSAPIExample) + // (note: Qt script engines automatically look for a ".toString" method on native classes when coercing values to strings) + 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 { + QVariantList values = keyValues.values(); + return 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& jsString, int compress_level = -1) const { + QByteArray arrayBuffer = qCompress(jsString.toUtf8(), compress_level); + return arrayBuffer; + } + QVariant qUncompressString(const QByteArray& arrayBuffer) const { + QString jsString = QString::fromUtf8(qUncompress(arrayBuffer)); + return jsString; + } + + /** + * 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.getValue("timestamp")); + * settings.setValue("timestamp", Date.now()); + * print("(after) example::timestamp", settings.getValue("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 }; + }; + + // JSSettingsHelper wraps a scoped (prefixed/separate) QSettings and exposes a subset of QSetting APIs as slots + class JSSettingsHelper : public QObject { + Q_OBJECT + public: + JSSettingsHelper(const QString& scope, QObject* parent = nullptr); + ~JSSettingsHelper(); + operator bool() const; + public slots: + QString fileName() const; + QString toString() const; + QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant()); + bool setValue(const QString& key, const QVariant& value); + QStringList allKeys() const; + protected: + QString _scope; + QString _fileName; + QSharedPointer _settings; + QString getLocalSettingsPath(const QString& scope) const; + }; + + // verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance + 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); + } + + // -------------------------------------------------- + // ----- inline JSSettingsHelper implementation ----- + JSSettingsHelper::operator bool() const { + return (bool)_settings; + } + JSSettingsHelper::JSSettingsHelper(const QString& scope, QObject* parent) : + QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)), + _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) { + } + JSSettingsHelper::~JSSettingsHelper() { + qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this; + } + QString JSSettingsHelper::fileName() const { + return _settings ? _settings->fileName() : ""; + } + QString JSSettingsHelper::toString() const { + return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings); + } + QVariant JSSettingsHelper::getValue(const QString& key, const QVariant& defaultValue) { + return _settings ? _settings->value(key, defaultValue) : defaultValue; + } + bool JSSettingsHelper::setValue(const QString& key, const QVariant& value) { + if (_settings) { + if (value.isValid()) { + _settings->setValue(key, value); + } else { + _settings->remove(key); + } + return true; + } + return false; + } + QStringList JSSettingsHelper::allKeys() const { + return _settings ? _settings->allKeys() : QStringList{}; + } + QString JSSettingsHelper::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); + } + // ----- /inline JSSettingsHelper implementation ----- } // namespace REPLACE_ME_WITH_UNIQUE_NAME