History

changes per CR

humbletim committed 6 years ago

Commit 5c2a8bd1 parent dedf6a61 file · +202 −201Browse files

Showing the changes to plugins/JSAPIExample/src/JSAPIExample.cpp (1 of 1 file). Show all

Unified · Split

plugins/JSAPIExample/src/JSAPIExample.cpp+202−201
@@ -28,222 +28,223 @@ · 27 unmodified lines
28// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes)28// NOTE: replace this with your own namespace when starting a new plugin (to avoid .so/.dll symbol clashes)
29namespace REPLACE_ME_WITH_UNIQUE_NAME {29namespace REPLACE_ME_WITH_UNIQUE_NAME {
30 30
31static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1";31 static constexpr auto JSAPI_SEMANTIC_VERSION = "0.0.1";
32static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample";32 static constexpr auto JSAPI_EXPORT_NAME = "JSAPIExample";
33 33
34QLoggingCategory logger{ "jsapiexample" };34 QLoggingCategory logger { "jsapiexample" };
35 35
36inline QVariant raiseScriptingError(QScriptContext* context, const QString& message, const QVariant& returnValue = QVariant()) {36 inline QVariant raiseScriptingError(QScriptContext* context, const QString& message, const QVariant& returnValue = QVariant()) {
37 if (context) {37 if (context) {
38 // when a QScriptContext is available throw an actual JS Exception (which can be caught using try/catch on JS side)38 // when a QScriptContext is available throw an actual JS Exception (which can be caught using try/catch on JS side)
39 context->throwError(message);39 context->throwError(message);
40 } else {40 } else {
41 // otherwise just log the error41 // otherwise just log the error
42 qCWarning(logger) << "error:" << message;42 qCWarning(logger) << "error:" << message;
43 }
44 return returnValue;
45}
46
47QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error);
48
49class JSAPIExample : public QObject, public QScriptable {
50 Q_OBJECT
51 Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json")
52 Q_PROPERTY(QString version MEMBER _version CONSTANT)
53public:
54 JSAPIExample() {
55 setObjectName(JSAPI_EXPORT_NAME);
56 auto scriptInit = DependencyManager::get<ScriptInitializers>();
57 if (!scriptInit) {
58 qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this;
59 return;
60 }43 }
61 qCWarning(logger) << "registering w/ScriptInitializerMixin..." << scriptInit.data();44 return returnValue;
62 scriptInit->registerScriptInitializer([this](QScriptEngine* engine) {
63 auto value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater);
64 engine->globalObject().setProperty(objectName(), value);
65 // qCDebug(logger) << "setGlobalInstance" << objectName() << engine->property("fileName");
66 });
67 // qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread();
68 }45 }
69 46
70 // NOTES: everything within the "public slots:" section below will be available from JS via overall plugin QObject47 QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error);
71 // also, to demonstrate future-proofing JS API code, QVariant's are used throughout most of these examples --48
72 // which still makes them very Qt-specific, but avoids depending directly on deprecated QtScript/QScriptValue APIs.49 class JSAPIExample : public QObject, public QScriptable {
73 // (as such this plugin class and its methods remain forward-compatible with other engines like QML's QJSEngine)50 Q_OBJECT
74 51 Q_PLUGIN_METADATA(IID "JSAPIExample" FILE "plugin.json")
75public slots:52 Q_PROPERTY(QString version MEMBER _version CONSTANT)
76 // pretty-printed representation for logging eg: print(JSAPIExample)53 public:
77 // (note: Qt script engines automatically look for a ".toString" method on native classes when coercing values to strings)54 JSAPIExample() {
78 QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); }55 setObjectName(JSAPI_EXPORT_NAME);
79 56 auto scriptInit = DependencyManager::get<ScriptInitializers>();
80 /**jsdoc57 if (!scriptInit) {
81 * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms58 qCWarning(logger) << "COULD NOT INITIALIZE (ScriptInitializers unavailable)" << qApp << this;
82 * @example <caption>Measure current setTimeout accuracy.</caption>59 return;
83 * var expected = 1000;60 }
84 * var start = JSAPIExample.now();61 qCWarning(logger) << "registering w/ScriptInitializerMixin..." << scriptInit.data();
85 * Script.setTimeout(function () {62 scriptInit->registerScriptInitializer([this](QScriptEngine* engine) {
86 * var elapsed = (JSAPIExample.now() - start)/1000;63 auto value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater);
87 * print("expected (ms):", expected, "actual (ms):", elapsed);64 engine->globalObject().setProperty(objectName(), value);
88 * }, expected);65 // qCDebug(logger) << "setGlobalInstance" << objectName() << engine->property("fileName");
89 */66 });
90 QVariant now() const { return usecTimestampNow(); }67 // qCInfo(logger) << "plugin loaded" << qApp << toString() << QThread::currentThread();
91
92 /**jsdoc
93 * Example of returning a JS Object key-value map
94 * @example <caption>"zip" a list of keys and corresponding values to form key-value map</caption>
95 * print(JSON.stringify(JSAPIExample.zip(["a","b"], [1,2])); // { "a": 1, "b": 2 }
96 */
97 QVariant zip(const QStringList& keys, const QVariantList& values) const {
98 QVariantMap out;
99 for (int i = 0; i < keys.size(); i++) {
100 out[keys[i]] = i < values.size() ? values[i] : QVariant();
101 }68 }
102 return out;
103 }
104 69
105 /**jsdoc70 // NOTES: everything within the "public slots:" section below will be available from JS via overall plugin QObject
106 * Example of returning a JS Array result71 // also, to demonstrate future-proofing JS API code, QVariant's are used throughout most of these examples --
107 * @example <caption>emulate Object.values(keyValues)</caption>72 // which still makes them very Qt-specific, but avoids depending directly on deprecated QtScript/QScriptValue APIs.
108 * print(JSON.stringify(JSAPIExample.values({ "a": 1, "b": 2 }))); // [1,2]73 // (as such this plugin class and its methods remain forward-compatible with other engines like QML's QJSEngine)
109 */74
110 QVariant values(const QVariantMap& keyValues) const {75 public slots:
111 QVariantList values = keyValues.values();76 // pretty-printed representation for logging eg: print(JSAPIExample)
112 return values;77 // (note: Qt script engines automatically look for a ".toString" method on native classes when coercing values to strings)
113 }78 QString toString() const { return QString("[%1 version=%2]").arg(objectName()).arg(_version); }
79
80 /**jsdoc
81 * Returns current microseconds (usecs) since Epoch. note: 1000usecs == 1ms
82 * @example <caption>Measure current setTimeout accuracy.</caption>
83 * var expected = 1000;
84 * var start = JSAPIExample.now();
85 * Script.setTimeout(function () {
86 * var elapsed = (JSAPIExample.now() - start)/1000;
87 * print("expected (ms):", expected, "actual (ms):", elapsed);
88 * }, expected);
89 */
90 QVariant now() const { return usecTimestampNow(); }
91
92 /**jsdoc
93 * Example of returning a JS Object key-value map
94 * @example <caption>"zip" a list of keys and corresponding values to form key-value map</caption>
95 * print(JSON.stringify(JSAPIExample.zip(["a","b"], [1,2])); // { "a": 1, "b": 2 }
96 */
97 QVariant zip(const QStringList& keys, const QVariantList& values) const {
98 QVariantMap out;
99 for (int i = 0; i < keys.size(); i++) {
100 out[keys[i]] = i < values.size() ? values[i] : QVariant();
101 }
102 return out;
103 }
114 104
115 /**jsdoc105 /**jsdoc
116 * Another example of returning JS Array data106 * Example of returning a JS Array result
117 * @example <caption>generate an integer sequence (inclusive of [from, to])</caption>107 * @example <caption>emulate Object.values(keyValues)</caption>
118 * print(JSON.stringify(JSAPIExample.seq(1,5)));// [1,2,3,4,5]108 * print(JSON.stringify(JSAPIExample.values({ "a": 1, "b": 2 }))); // [1,2]
119 */109 */
120 QVariant seq(int from, int to) const {110 QVariant values(const QVariantMap& keyValues) const {
121 QVariantList out;111 QVariantList values = keyValues.values();
122 for (int i = from; i <= to; i++) {112 return values;
123 out.append(i);
124 }113 }
125 return out;
126 }
127 114
128 /**jsdoc115 /**jsdoc
129 * Example of returning arbitrary binary data from C++ (resulting in a JS ArrayBuffer)116 * Another example of returning JS Array data
130 * see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#Examples117 * @example <caption>generate an integer sequence (inclusive of [from, to])</caption>
131 * @example <caption>return compressed/decompressed versions of the input data</caption>118 * print(JSON.stringify(JSAPIExample.seq(1,5)));// [1,2,3,4,5]
132 * var data = "testing 1 2 3";119 */
133 * var z = JSAPIExample.qCompressString(data); // z will be an ArrayBuffer120 QVariant seq(int from, int to) const {
134 * var u = JSAPIExample.qUncompressString(z); // u will be a String value121 QVariantList out;
135 * print(JSON.stringify({ input: data, compressed: z.byteLength, output: u, uncompressed: u.length }));122 for (int i = from; i <= to; i++) {
136 */123 out.append(i);
137 QVariant qCompressString(const QString& jsString, int compress_level = -1) const {124 }
138 QByteArray arrayBuffer = qCompress(jsString.toUtf8(), compress_level);125 return out;
139 return arrayBuffer;126 }
140 }127
141 QVariant qUncompressString(const QByteArray& arrayBuffer) const {128 /**jsdoc
142 QString jsString = QString::fromUtf8(qUncompress(arrayBuffer));129 * Example of returning arbitrary binary data from C++ (resulting in a JS ArrayBuffer)
143 return jsString;130 * see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer#Examples
144 }131 * @example <caption>return compressed/decompressed versions of the input data</caption>
132 * var data = "testing 1 2 3";
133 * var z = JSAPIExample.qCompressString(data); // z will be an ArrayBuffer
134 * var u = JSAPIExample.qUncompressString(z); // u will be a String value
135 * print(JSON.stringify({ input: data, compressed: z.byteLength, output: u, uncompressed: u.length }));
136 */
137 QVariant qCompressString(const QString& jsString, int compress_level = -1) const {
138 QByteArray arrayBuffer = qCompress(jsString.toUtf8(), compress_level);
139 return arrayBuffer;
140 }
141 QVariant qUncompressString(const QByteArray& arrayBuffer) const {
142 QString jsString = QString::fromUtf8(qUncompress(arrayBuffer));
143 return jsString;
144 }
145
146 /**
147 * Example of exposing a custom "managed" C++ QObject to JS
148 * The lifecycle of the created QObject* instance becomes managed by the invoking QScriptEngine --
149 * it will be automatically cleaned up once no longer reachable from any JS variables/closures.
150 * @example <caption>access persistent settings stored in separate .json files</caption>
151 * var settings = JSAPIExample.getScopedSettings("example");
152 * print("example settings stored in:", settings.fileName());
153 * print("(before) example::timestamp", settings.getValue("timestamp"));
154 * settings.setValue("timestamp", Date.now());
155 * print("(after) example::timestamp", settings.getValue("timestamp"));
156 * print("all example::* keys", settings.allKeys());
157 * settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector
158 */
159 QScriptValue getScopedSettings(const QString& scope) {
160 auto engine = QScriptable::engine();
161 if (!engine) {
162 return QScriptValue::NullValue;
163 }
164 QString error;
165 auto cppValue = createScopedSettings(scope, engine, error);
166 if (!cppValue) {
167 raiseScriptingError(context(), "error creating scoped settings instance: " + error);
168 return QScriptValue::NullValue;
169 }
170 return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater);
171 }
145 172
146 /**173 private:
147 * Example of exposing a custom "managed" C++ QObject to JS174 const QString _version { JSAPI_SEMANTIC_VERSION };
148 * The lifecycle of the created QObject* instance becomes managed by the invoking QScriptEngine --175 };
149 * it will be automatically cleaned up once no longer reachable from any JS variables/closures.176
150 * @example <caption>access persistent settings stored in separate .json files</caption>177 // JSSettingsHelper wraps a scoped (prefixed/separate) QSettings and exposes a subset of QSetting APIs as slots
151 * var settings = JSAPIExample.getScopedSettings("example");178 class JSSettingsHelper : public QObject {
152 * print("example settings stored in:", settings.fileName());179 Q_OBJECT
153 * print("(before) example::timestamp", settings.getValue("timestamp"));180 public:
154 * settings.setValue("timestamp", Date.now());181 JSSettingsHelper(const QString& scope, QObject* parent = nullptr);
155 * print("(after) example::timestamp", settings.getValue("timestamp"));182 ~JSSettingsHelper();
156 * print("all example::* keys", settings.allKeys());183 operator bool() const;
157 * settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector184 public slots:
158 */185 QString fileName() const;
159 QScriptValue getScopedSettings(const QString& scope) {186 QString toString() const;
160 auto engine = QScriptable::engine();187 QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant());
161 if (!engine)188 bool setValue(const QString& key, const QVariant& value);
162 return QScriptValue::NullValue;189 QStringList allKeys() const;
163 QString error;190 protected:
164 auto cppValue = createScopedSettings(scope, engine, error);191 QString _scope;
165 if (!cppValue) {192 QString _fileName;
166 raiseScriptingError(context(), "error creating scoped settings instance: " + error);193 QSharedPointer<QSettings> _settings;
167 return QScriptValue::NullValue;194 QString getLocalSettingsPath(const QString& scope) const;
195 };
196
197 // verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance
198 QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error) {
199 const QRegExp VALID_SETTINGS_SCOPE { "[-_A-Za-z0-9]{1,64}" };
200 if (!VALID_SETTINGS_SCOPE.exactMatch(scope)) {
201 error = QString("invalid scope (expected alphanumeric <= 64 chars not '%1')").arg(scope);
202 return nullptr;
168 }203 }
169 return engine->newQObject(cppValue, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeDeleteLater);204 return new JSSettingsHelper(scope, parent);
170 }205 }
171 206
172private:207 // --------------------------------------------------
173 const QString _version{ JSAPI_SEMANTIC_VERSION };208 // ----- inline JSSettingsHelper implementation -----
174};209 JSSettingsHelper::operator bool() const {
175 210 return (bool)_settings;
176// JSSettingsHelper wraps a scoped (prefixed/separate) QSettings and exposes a subset of QSetting APIs as slots
177class JSSettingsHelper : public QObject {
178 Q_OBJECT
179public:
180 JSSettingsHelper(const QString& scope, QObject* parent = nullptr);
181 ~JSSettingsHelper();
182 operator bool() const;
183public slots:
184 QString fileName() const;
185 QString toString() const;
186 QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant());
187 bool setValue(const QString& key, const QVariant& value);
188 QStringList allKeys() const;
189protected:
190 QString _scope;
191 QString _fileName;
192 QSharedPointer<QSettings> _settings;
193 QString getLocalSettingsPath(const QString& scope) const;
194};
195
196// verifies the requested scope is sensible and creates/returns a scoped JSSettingsHelper instance
197QObject* createScopedSettings(const QString& scope, QObject* parent, QString& error) {
198 const QRegExp VALID_SETTINGS_SCOPE{ "[-_A-Za-z0-9]{1,64}" };
199 if (!VALID_SETTINGS_SCOPE.exactMatch(scope)) {
200 error = QString("invalid scope (expected alphanumeric <= 64 chars not '%1')").arg(scope);
201 return nullptr;
202 }211 }
203 return new JSSettingsHelper(scope, parent);212 JSSettingsHelper::JSSettingsHelper(const QString& scope, QObject* parent) :
204}213 QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)),
205 214 _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {
206// --------------------------------------------------215 }
207// ----- inline JSSettingsHelper implementation -----216 JSSettingsHelper::~JSSettingsHelper() {
208JSSettingsHelper::operator bool() const {217 qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this;
209 return (bool)_settings;218 }
210}219 QString JSSettingsHelper::fileName() const {
211JSSettingsHelper::JSSettingsHelper(const QString& scope, QObject* parent) :220 return _settings ? _settings->fileName() : "";
212 QObject(parent), _scope(scope), _fileName(getLocalSettingsPath(scope)),221 }
213 _settings(_fileName.isEmpty() ? nullptr : new QSettings(_fileName, JSON_FORMAT)) {222 QString JSSettingsHelper::toString() const {
214}223 return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings);
215JSSettingsHelper::~JSSettingsHelper() {224 }
216 qCDebug(logger) << "~JSSettingsHelper" << _scope << _fileName << this;225 QVariant JSSettingsHelper::getValue(const QString& key, const QVariant& defaultValue) {
217}226 return _settings ? _settings->value(key, defaultValue) : defaultValue;
218QString JSSettingsHelper::fileName() const {227 }
219 return _settings ? _settings->fileName() : "";228 bool JSSettingsHelper::setValue(const QString& key, const QVariant& value) {
220}229 if (_settings) {
221QString JSSettingsHelper::toString() const {230 if (value.isValid()) {
222 return QString("[JSSettingsHelper scope=%1 valid=%2]").arg(_scope).arg((bool)_settings);231 _settings->setValue(key, value);
223}232 } else {
224QVariant JSSettingsHelper::getValue(const QString& key, const QVariant& defaultValue) {233 _settings->remove(key);
225 return _settings ? _settings->value(key, defaultValue) : defaultValue;234 }
226}235 return true;
227bool JSSettingsHelper::setValue(const QString& key, const QVariant& value) {
228 if (_settings) {
229 if (value.isValid()) {
230 _settings->setValue(key, value);
231 } else {
232 _settings->remove(key);
233 }236 }
234 return true;237 return false;
238 }
239 QStringList JSSettingsHelper::allKeys() const {
240 return _settings ? _settings->allKeys() : QStringList{};
241 }
242 QString JSSettingsHelper::getLocalSettingsPath(const QString& scope) const {
243 // generate a prefixed filename (relative to the main application's Interface.json file)
244 const QString fileName = QString("jsapi_%1.json").arg(scope);
245 return QFileInfo(::settingsFilename()).dir().filePath(fileName);
235 }246 }
236 return false;247 // ----- /inline JSSettingsHelper implementation -----
237}
238QStringList JSSettingsHelper::allKeys() const {
239 return _settings ? _settings->allKeys() : QStringList{};
240}
241QString JSSettingsHelper::getLocalSettingsPath(const QString& scope) const {
242 // generate a prefixed filename (relative to the main application's Interface.json file)
243 const QString fileName = QString("jsapi_%1.json").arg(scope);
244 return QFileInfo(::settingsFilename()).dir().filePath(fileName);
245}
246// ----- /inline JSSettingsHelper implementation -----
247 248
248} // namespace REPLACE_ME_WITH_UNIQUE_NAME249} // namespace REPLACE_ME_WITH_UNIQUE_NAME
249 250