2#include <JS/Qml/QmlObjects.hpp>
3#include <JS/Commands/EditScript.hpp>
4#include <score/command/Dispatchers/CommandDispatcher.hpp>
5#include <score/document/DocumentInterface.hpp>
6#include <Library/LibrarySettings.hpp>
7#include <score/application/ApplicationContext.hpp>
8#include <score/application/GUIApplicationContext.hpp>
10#include <ossia/detail/logger.hpp>
11#include <ossia-qt/invoke.hpp>
12#include <ossia-qt/qml_engine_functions.hpp>
13#include <JS/ConsolePanel.hpp>
18#include <QCryptographicHash>
19#include <QQmlAbstractUrlInterceptor>
20#include <QReadWriteLock>
21#include <QStandardPaths>
25#include <QQmlComponent>
33inline void connectStateCommit(Script* script, ProcessModel* model)
38 script, &Script::commitState, model,
39 [context = QPointer{model}](
const QString& key,
const QJSValue& value) {
40 auto converted = ossia::qt::value_from_js(value);
41 QMetaObject::invokeMethod(
42 qApp, [context, key, value = std::move(converted)] {
45 const auto& state = context->state();
46 if(
auto it = state.find(key); it != state.end() && it->second == value)
49 score::IDocument::documentContext(*context).commandStack};
50 dispatcher.submit<UpdateStateElement>(*context, key, value);
52 Qt::QueuedConnection);
54 Qt::DirectConnection);
71inline bool loaderIsCurrent(QQmlEngine& e,
const QUrl& url,
const QByteArray& str)
73 using Record = std::pair<QPointer<QQmlEngine>, QHash<QUrl, QByteArray>>;
74 thread_local std::vector<Record> known;
76 auto it = std::find_if(
77 known.begin(), known.end(), [&](
const Record& r) { return r.first == &e; });
80 std::erase_if(known, [](
const Record& r) {
return r.first.isNull(); });
81 it = known.emplace(known.end(), &e, QHash<QUrl, QByteArray>{});
84 const auto digest = QCryptographicHash::hash(str, QCryptographicHash::Sha1);
85 auto& digests = it->second;
86 const auto known_digest = digests.constFind(url);
87 if(known_digest == digests.cend())
89 digests.insert(url, digest);
92 return *known_digest == digest;
103inline void loadJSObjectFromUrl(
104 const QUrl& url,
const QByteArray& str, QQmlComponent& comp)
106 if(
auto* engine = comp.engine(); engine && detail::loaderIsCurrent(*engine, url, str))
109 comp.setData(str, url);
113inline QString uiPathOf(
const QString& rootPath)
115 QString path = rootPath;
116 if(path.endsWith(
".qml"))
117 path.insert(path.size() - 4,
".ui");
127inline bool scriptIsItsFile(
const QString& path,
const QByteArray& str)
130 return f.open(QIODevice::ReadOnly) && f.readAll().trimmed() == str.trimmed();
145 QHash<QString, Entry> byDir;
159inline const QString& editStagingRoot()
161 static const QString root = [] {
162 const auto cache = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
163 return cache.isEmpty() ? QString{} : cache + QStringLiteral(
"/qml-edits");
177 QUrl intercept(
const QUrl& url, DataType)
override
179 const QString& root = editStagingRoot();
180 if(root.isEmpty() || !url.isLocalFile())
185 const QString path = url.toLocalFile();
186 if(!path.startsWith(root) || path.size() <= root.size() + 1
187 || path[root.size()] != QLatin1Char(
'/'))
190 const QString rest = path.mid(root.size() + 1);
191 const int slash = rest.indexOf(QLatin1Char(
'/'));
195 const QString wanted = rest.mid(slash + 1);
197 auto& s = detail::stagedScripts();
198 QReadLocker _{&s.lock};
199 const auto it = s.byDir.constFind(root + QLatin1Char(
'/') + rest.left(slash));
200 if(it == s.byDir.cend() || wanted == it->fileName)
202 return QUrl::fromLocalFile(it->originalDir + QLatin1Char(
'/') + wanted);
212 QQmlEngine* engine{};
216 engine->removeUrlInterceptor(&interceptor);
224inline void installEditRedirect(QQmlEngine& engine)
226 static constexpr auto marker =
"score_qml_edit_redirect";
227 if(engine.property(marker).toBool())
231 holder->engine = &engine;
232 holder->setParent(&engine);
233 engine.addUrlInterceptor(&holder->interceptor);
234 engine.setProperty(marker,
true);
240inline QString stagingFolderPrefix(
const QString& originalPath)
242 const QFileInfo fi{originalPath};
243 QString stem = fi.fileName();
245 if(!c.isLetterOrNumber())
246 c = QLatin1Char(
'_');
247 return stem + QLatin1Char(
'-')
248 + QString::fromLatin1(QCryptographicHash::hash(
249 fi.absolutePath().toUtf8(), QCryptographicHash::Sha1)
257inline void sweepStagedFolders(
258 const QString& root,
const QString& prefix,
const QString& keep)
261 const auto stale = dir.entryList({prefix +
"*"}, QDir::Dirs | QDir::NoDotAndDotDot);
262 auto& s = detail::stagedScripts();
263 for(
const auto& name : stale)
267 const QString path = dir.filePath(name);
268 if(QDir{path}.removeRecursively())
270 QWriteLocker _{&s.lock};
271 s.byDir.remove(path);
280inline QString stageEditedScript(
const QString& originalPath,
const QByteArray& str)
282 const QString root = editStagingRoot();
286 const QFileInfo fi{originalPath};
287 if(fi.fileName().isEmpty())
294 const QString name = QStringLiteral(
".score-edit")
295 + (originalPath.endsWith(QStringLiteral(
".ui.qml"))
296 ? QStringLiteral(
".ui.qml")
297 : QStringLiteral(
".qml"));
299 const QString prefix = stagingFolderPrefix(originalPath);
302 + QString::fromLatin1(
303 QCryptographicHash::hash(str, QCryptographicHash::Sha1).toHex().left(16));
304 const QString dir = root + QLatin1Char(
'/') + folder;
305 const QString file = dir + QLatin1Char(
'/') + name;
307 if(!QFileInfo::exists(file))
309 if(!QDir{}.mkpath(dir))
312 if(!f.open(QIODevice::WriteOnly))
314 const bool written = f.write(str) == str.size();
318 QDir{dir}.removeRecursively();
324 auto& s = detail::stagedScripts();
325 QWriteLocker _{&s.lock};
326 s.byDir.insert(dir, {name, fi.absolutePath()});
329 sweepStagedFolders(root, prefix, folder);
333inline void loadJSObjectFromString(
334 const QString& rootPath,
const QByteArray& str, QQmlComponent& comp,
bool is_ui)
336 const QString path = is_ui ? uiPathOf(rootPath) : rootPath;
338 if(scriptIsItsFile(path, str))
340 loadJSObjectFromUrl(QUrl::fromLocalFile(path), str, comp);
347 if(
auto* engine = comp.engine())
349 if(
const QString staged = stageEditedScript(path, str); !staged.isEmpty())
351 installEditRedirect(*engine);
352 loadJSObjectFromUrl(QUrl::fromLocalFile(staged), str, comp);
357 comp.setData(str, QUrl::fromLocalFile(path));
362inline void loadJSObjectFromFile(
const QString& path, QQmlComponent& comp)
365 if(!f.open(QIODevice::ReadOnly))
367 loadJSObjectFromUrl(QUrl::fromLocalFile(path), f.readAll(), comp);
370inline JS::Script* createJSObject(QQmlComponent& c, QQmlContext* context)
372 const auto& errs = c.errors();
375 ossia::logger().error(
376 "Uncaught exception at line {} : {}", errs[0].line(),
377 errs[0].toString().toStdString());
382 auto object = c.create(context);
383 auto obj = qobject_cast<JS::Script*>(
object);
392 const QString& rootPath,
const QString& val, QQmlEngine* engine,
393 QQmlContext* context)
395 if(val.trimmed().startsWith(
"import"))
397 QQmlComponent c{engine};
398 loadJSObjectFromString(rootPath, val.toUtf8(), c,
false);
399 return createJSObject(c, context);
401 else if(QFile::exists(val))
403 QQmlComponent c{engine};
404 loadJSObjectFromFile(val, c);
405 return createJSObject(c, context);
410inline void setupExecFuncs(
auto* self, QObject* context, ossia::qt::qml_engine_functions* m_execFuncs)
413 m_execFuncs, &ossia::qt::qml_engine_functions::system, qApp,
414 [](
const QString& code) {
415 std::thread{[code] { ::system(code.toStdString().c_str()); }}.detach();
416 }, Qt::QueuedConnection);
421 m_execFuncs, &ossia::qt::qml_engine_functions::exec, js_panel,
422 &JS::PanelDelegate::evaluate, Qt::QueuedConnection);
425 m_execFuncs, &ossia::qt::qml_engine_functions::compute, m_execFuncs,
426 [self, context, m_execFuncs, js_panel](
const QString& code,
const QString& cbname) {
431 , context=QPointer{context}
432 , cur = QPointer{self->m_object}
434 , cbname] (
const QVariant& v) {
439 ossia::qt::run_async(m_execFuncs, [self, context, cur, v, cbname] {
442 if(self->m_object != cur)
445 auto mo = self->m_object->metaObject();
446 for(
int i = 0; i < mo->methodCount(); i++)
448 if(mo->method(i).name() == cbname)
450 mo->method(i).invoke(
451 self->m_object, Qt::DirectConnection, QGenericReturnArgument(),
452 QArgument<QVariant>{
"v", v});
459 ossia::qt::run_async(js_panel, [js_panel, code, cb]() {
460 js_panel->compute(code, cb);
462 }, Qt::DirectConnection);
The CommandDispatcher class.
Definition CommandDispatcher.hpp:13
Definition ExecutionHelpers.hpp:175
Definition ConsolePanel.hpp:40
Definition QmlObjects.hpp:859
@ Record
The whole run becomes one command at stop.
Owns one redirect for the engine it is parented to.
Definition ExecutionHelpers.hpp:210
Definition ExecutionHelpers.hpp:140
QString originalDir
where everything else it asks for lives
Definition ExecutionHelpers.hpp:142
QString fileName
the staged script, the one file that is really there
Definition ExecutionHelpers.hpp:141
Definition ExecutionHelpers.hpp:138