• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 36095816006

25 Sep 2026 04:46AM UTC coverage: 42.219% (-0.01%) from 42.23%
36095816006

Pull #2304

github

web-flow
Merge 509979a4d into c0c64ec39
Pull Request #2304: QML API additions, and three teardown fixes

39 of 229 new or added lines in 13 files covered. (17.03%)

160 existing lines in 12 files now uncovered.

125818 of 298010 relevant lines covered (42.22%)

117820.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.68
/src/plugins/score-plugin-js/JS/ApplicationPlugin.cpp
1
#include "ApplicationPlugin.hpp"
2

3
#include <JS/DocumentPlugin.hpp>
4
#include <JS/Qml/DeviceContext.hpp>
5
#include <JS/Qml/EditContext.hpp>
6
#include <JS/Qml/Utils.hpp>
7
#include <JS/Qml/ViewContext.hpp>
8
#include <Library/LibrarySettings.hpp>
9
#include <LocalTree/LocalTreeDocumentPlugin.hpp>
10

11
#include <core/application/ApplicationInterface.hpp>
12
#include <core/document/Document.hpp>
13
#include <core/presenter/DocumentManager.hpp>
14

15
#include <ossia/detail/thread.hpp>
16

17
#include <ossia-qt/invoke.hpp>
18
#include <ossia-qt/qml_protocols.hpp>
19

20
#include <QCommandLineParser>
21
#include <QFile>
22
#include <QFileInfo>
23
#include <QJsonDocument>
24
#include <QString>
25

26
#if __has_include(<QQuickWindow>)
27
#include <QGuiApplication>
28
#include <QQuickItem>
29
#include <QQuickWindow>
30
#endif
31

32
#if SCORE_HAS_GPU_JS
33
#include <Gfx/Settings/Model.hpp>
34
#endif
35

36
#include <ossia/network/context.hpp>
37

38
namespace JS
39
{
40
// Check whether the input is a script, or a file path.
41
// An existing file always wins: a real path may legitimately contain
42
// characters (parentheses, braces, ...) that also occur in inline source,
43
// so the file-existence check must come FIRST. Only when the input is not
44
// an existing file do we fall back to the inline-source heuristic.
45
static bool stringIsScript(const QString& input)
64✔
46
{
47
  if(input.isEmpty())
64✔
48
    return false;
×
49

50
  if(QFileInfo fileInfo{input}; fileInfo.exists() && fileInfo.isFile())
124✔
51
    return false;
60✔
52

53
  if(input.length() > 4096)
4✔
54
    return true;
×
55

56
  for(QChar ch : input)
56✔
57
  {
58
    const char16_t c = ch.unicode();
55✔
59
    if(c == '\n' || c == '\r' || c == ';' || c == '{' || c == '}' || c == '('
55✔
60
       || c == ')')
55✔
61
      return true;
3✔
62
  }
63

64
  return false;
1✔
65
}
64✔
66

67
ApplicationPlugin::ApplicationPlugin(const score::GUIApplicationContext& ctx)
1,852✔
68
    : score::GUIApplicationPlugin{ctx}
1,852✔
69
{
1,852✔
70
#if __has_include(<QQuickWindow>) && !defined(__APPLE__)
71
  // Crisp text in every QML UI: distance-field rendering looks blurry at the small
72
  // font sizes our panels use, native glyph rendering matches the rest of score.
73
  // Not on macOS: https://qt-project.atlassian.net/browse/QTBUG-150490
74
  QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
1,852✔
75
#endif
76

77
  // For the console
78
  m_consoleEngine.globalObject().setProperty("Score", m_consoleEngine.newQObject(new EditJsContext));
1,852✔
79
  m_consoleEngine.globalObject().setProperty("Util", m_consoleEngine.newQObject(new JsUtils));
1,852✔
80
  m_consoleEngine.globalObject().setProperty(
3,704✔
81
      "System", m_consoleEngine.newQObject(new JsSystem));
1,852✔
82
  m_consoleEngine.globalObject().setProperty(
3,704✔
83
      "Library", m_consoleEngine.newQObject(new JsLibrary));
1,852✔
84
  m_consoleEngine.globalObject().setProperty("Device", m_consoleEngine.newQObject(new DeviceContext{m_consoleEngine}));
1,852✔
85
  m_consoleEngine.globalObject().setProperty("View", m_consoleEngine.newQObject(new JsViewContext));
1,852✔
86
  connect(&m_consoleEngine, &QQmlEngine::exit, this, [&](int retCode) {
1,896✔
87
    for(auto& doc : score::GUIAppContext().docManager.documents())
88✔
88
      doc->commandStack().markCurrentIndexAsSaved();
44✔
89
    // quit() is exit(0), which discarded the code Qt.exit() was given: a script
90
    // could stop the app but never report that it had failed.
91
    qApp->exit(retCode);
44✔
92
    QTimer::singleShot(
44✔
93
        500, [] { score::GUIApplicationInterface::instance().forceExit(); });
11✔
94
  });
44✔
95
  m_asioContext = std::make_shared<ossia::net::network_context>();
1,852✔
96
  m_processMessages = true;
1,852✔
97
  m_consoleEngine.globalObject().setProperty(
3,704✔
98
      "Protocols",
1,852✔
99
      m_consoleEngine.newQObject(new ossia::qt::qml_protocols{m_asioContext, this}));
1,852✔
100
  m_asioThread = std::thread{[this] {
3,704✔
101
    ossia::set_thread_name("ossia app asio");
1,852✔
102
    while(m_processMessages)
3,704✔
103
    {
104
      m_asioContext->run();
1,852✔
105
    }
106
  }};
1,852✔
107

108
  // For scripts of processes that run in the ui thread:
109
  m_scriptProcessUIEngine.globalObject().setProperty(
3,704✔
110
      "Util", m_scriptProcessUIEngine.newQObject(new JsUtils));
1,852✔
111
  m_scriptProcessUIEngine.globalObject().setProperty(
3,704✔
112
      "System", m_scriptProcessUIEngine.newQObject(new JsSystem));
1,852✔
113
  m_scriptProcessUIEngine.globalObject().setProperty(
3,704✔
114
      "Library", m_scriptProcessUIEngine.newQObject(new JsLibrary));
1,852✔
115
  m_scriptProcessUIEngine.globalObject().setProperty(
3,704✔
116
      "View", m_scriptProcessUIEngine.newQObject(new JsViewContext));
1,852✔
117

118
  // Command-line option parsing
119
  QCommandLineParser parser;
1,852✔
120

121
  QCommandLineOption script_opt(
1,852✔
122
      "script", QCoreApplication::translate("js", "script"), "Script", "");
1,852✔
123
  parser.addOption(script_opt);
1,852✔
124

125
  parser.parse(ctx.applicationSettings.arguments);
1,852✔
126
  for(const QString& script : parser.values(script_opt))
1,916✔
127
  {
128
    if(script.isEmpty())
64✔
129
      continue;
×
130

131
    if(stringIsScript(script))
64✔
132
    {
133
      this->m_start_scripts.push_back(StartScript{.source = script});
3✔
134
      continue;
135
    }
136

137
    QFile f{script};
61✔
138
    if(!f.open(QIODevice::ReadOnly))
61✔
139
    {
140
      qCritical() << "--script: cannot open" << script << ":" << f.errorString();
1✔
141
      this->m_start_script_failed = true;
1✔
142
      continue;
1✔
143
    }
144

145
    const QFileInfo fi{f};
60✔
146
    StartScript s;
60✔
147
    s.name = script;
60✔
148
    s.file = fi.canonicalFilePath();
60✔
149
    s.dir = fi.canonicalPath();
60✔
150
    // .mjs is what the rest of score calls an ES module (see the library's
151
    // ModuleLibraryHandler); only those go through importModule().
152
    s.module = fi.suffix().compare(QStringLiteral("mjs"), Qt::CaseInsensitive) == 0;
60✔
153
    if(!s.module)
60✔
154
      s.source = QString::fromUtf8(f.readAll());
56✔
155

156
    this->m_start_scripts.push_back(std::move(s));
60✔
157
  }
61✔
158
}
×
159

NEW
160
void ApplicationPlugin::registerCommandHandler(const QString& name, const QJSValue& fn)
×
161
{
NEW
162
  if(!fn.isCallable())
×
163
  {
NEW
164
    qWarning() << "Score.registerCommandHandler:" << name << "is not a function";
×
NEW
165
    return;
×
166
  }
NEW
167
  m_commandHandlers.insert(name, fn);
×
NEW
168
  m_reportedMissingHandlers.remove(name);
×
NEW
169
}
×
170

NEW
171
void ApplicationPlugin::callCommandHandler(
×
172
    const QString& name, const QByteArray& payload)
173
{
NEW
174
  auto it = m_commandHandlers.find(name);
×
NEW
175
  if(it == m_commandHandlers.end())
×
176
  {
177
    // A backup restored before the script that owns the handler has loaded,
178
    // or a handler that was renamed: the edit cannot be replayed. Say so once
179
    // per name rather than on every undo.
NEW
180
    if(!m_reportedMissingHandlers.contains(name))
×
181
    {
NEW
182
      m_reportedMissingHandlers.insert(name);
×
NEW
183
      qWarning() << "ScriptCommand: no handler registered for" << name
×
NEW
184
                 << "- the edit was not replayed";
×
NEW
185
    }
×
NEW
186
    return;
×
187
  }
188

NEW
189
  QJSValue arg;
×
NEW
190
  if(!payload.isEmpty())
×
191
  {
NEW
192
    QJsonParseError err{};
×
193
    const auto doc = QJsonDocument::fromJson(payload, &err);
NEW
194
    if(err.error != QJsonParseError::NoError)
×
195
    {
NEW
196
      qWarning() << "ScriptCommand: payload for" << name
×
NEW
197
                 << "is not JSON:" << err.errorString();
×
NEW
198
      return;
×
199
    }
200
    arg = m_consoleEngine.toScriptValue(doc.toVariant());
NEW
201
  }
×
NEW
202
  auto res = it->call({arg});
×
NEW
203
  if(res.isError())
×
NEW
204
    qWarning() << "ScriptCommand: handler" << name << "failed:" << res.toString();
×
NEW
205
}
×
206

207
QJSValue ApplicationPlugin::importModule(QQmlEngine& engine, const QString& path)
4✔
208
{
209
  QJSValue mod = engine.importModule(path);
4✔
210
  if(mod.isError())
4✔
211
    return mod;
1✔
212

213
  if(auto init = mod.property("initialize"); init.isCallable())
5✔
214
    if(const auto res = init.call(); res.isError())
4✔
215
      return res;
1✔
216

217
  engine.globalObject().setProperty(QFileInfo{path}.baseName(), mod);
2✔
218
  return mod;
2✔
219
}
4✔
220

221
void ApplicationPlugin::on_newDocument(score::Document& doc)
623✔
222
{
223
  score::addDocumentPlugin<DocumentPlugin>(doc);
623✔
224
}
623✔
225

226
ApplicationPlugin::~ApplicationPlugin()
3,702✔
227
{
1,851✔
228
  m_processMessages = false;
1,851✔
229
  m_asioContext->context.stop();
1,851✔
230
  m_asioThread.join();
1,851✔
231
}
3,702✔
232

233
void ApplicationPlugin::on_createdDocument(score::Document& doc)
655✔
234
{
235
  // Local Tree
236
  LocalTree::DocumentPlugin* lt = doc.context().findPlugin<LocalTree::DocumentPlugin>();
655✔
237
  if(lt)
655✔
238
  {
239
    auto& root = lt->device().get_root_node();
655✔
240

241
    auto node = root.create_child("script");
655✔
242
    auto address = node->create_parameter(ossia::val_type::STRING);
655✔
243
    address->set_value(std::string{});
655✔
244
    address->set_access(ossia::access_mode::SET);
655✔
245
    address->add_callback([&](const ossia::value& v) {
685✔
246
      ossia::qt::run_async(
30✔
247
          this, [this, str = QString::fromStdString(ossia::convert<std::string>(v))] {
60✔
248
        auto res = m_consoleEngine.evaluate(str);
30✔
249
        if(res.isError())
30✔
250
        {
251
          qDebug() << res.toString();
20✔
252
        }
20✔
253
      });
30✔
254
    });
30✔
255
  }
655✔
256

257
  // Custom data
258
  if(auto customData = doc.context().findPlugin<DocumentPlugin>(); !customData)
655✔
259
    score::addDocumentPlugin<DocumentPlugin>(doc);
×
260

261
  if(m_start_script_failed)
655✔
262
  {
263
    qGuiApp->exit(2);
1✔
264
    return;
1✔
265
  }
266

267
}
655✔
268

269
void ApplicationPlugin::runStartScripts()
48✔
270
{
271
  for(const StartScript& s : m_start_scripts)
93✔
272
  {
273
    if(!s.dir.isEmpty())
49✔
274
      m_consoleEngine.addImportPath(s.dir);
47✔
275

276
    // Report a throwing --script: an unresolvable readFile returns an empty
277
    // string and eval("") is a no-op, so the process would otherwise exit
278
    // reporting success and a harness could not tell that from a pass.
279
    const auto res = s.module ? importModule(m_consoleEngine, s.file)
49✔
280
                              : m_consoleEngine.evaluate(s.source, s.name);
45✔
281
    if(res.isError())
49✔
282
    {
283
      qCritical().noquote()
8✔
284
          << "--script:" << (s.name.isEmpty() ? QStringLiteral("<inline>") : s.name)
4✔
285
          << "line" << res.property("lineNumber").toInt() << ":" << res.toString();
4✔
286
      qGuiApp->exit(3);
4✔
287
      return;
4✔
288
    }
289
  }
49✔
290
}
48✔
291
void ApplicationPlugin::initialize()
1,852✔
292
{
293
  // Documents named on the command line are loaded before afterStartup(), and
294
  // compiling one of their scripts is what makes the library's QML modules
295
  // needed: the import paths have to be in place by then.
296
  for(auto& p : this->context.settings<Library::Settings::Model>().getIncludePaths())
1,852✔
297
  {
298
    m_scriptProcessUIEngine.addImportPath(p);
×
299
    // The console engine runs --script, the console panel and every library
300
    // .mjs; without this they could not import what a JS process can, which
301
    // made the same `import` line work in a process and fail in a script.
302
    m_consoleEngine.addImportPath(p);
×
303
  }
304
}
1,852✔
305

306
void ApplicationPlugin::afterStartup()
62✔
307
{
308
  if(!m_start_scripts.empty())
62✔
309
  {
310
    QTimer::singleShot(
61✔
311
        (1 + context.applicationSettings.waitAfterLoad) * 1000, this,
61✔
312
        [this] { runStartScripts(); });
109✔
313
  }
61✔
314

315
#if __has_include(<QQuickWindow>)
316
  if(QFileInfo f{context.applicationSettings.ui}; f.isFile())
66✔
317
  {
318
    m_comp = new QQmlComponent{&m_consoleEngine, f.absoluteFilePath(), this};
4✔
319

320
    if(auto obj = m_comp->create())
4✔
321
    {
322
      if(auto item = qobject_cast<QQuickItem*>(obj))
1✔
323
      {
324
        m_window = new QQuickWindow{};
×
325
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
326
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
327
  // leaves the window with no title bar, close or minimise button.
328
  m_window->setFlags(
×
329
      m_window->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
×
330
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
×
331
      | Qt::WindowMaximizeButtonHint);
×
332
#if defined(__EMSCRIPTEN__)
333
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
334
  // turns into showFullScreen() for every top level: the requested size is
335
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
336
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
337
  m_window->setFlags(m_window->flags() | Qt::Dialog);
338
#endif
339
        m_window->setWidth(640);
×
340
        m_window->setHeight(480);
×
341
        item->setParentItem(m_window->contentItem());
×
342
#if defined(__EMSCRIPTEN__)
343
        m_window->showNormal();
344
#else
345
        m_window->show();
×
346
#endif
347
        return;
×
348
      }
349
    }
1✔
350
    else
351
    {
352
      qDebug() << m_comp->errorString();
3✔
353
      qGuiApp->exit(1);
3✔
354
    }
355
    delete m_comp;
4✔
356
  }
4✔
357
#endif
358
}
62✔
359
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc