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

ossia / score / 36173900300

25 Sep 2026 06:30PM UTC coverage: 42.234% (+0.004%) from 42.23%
36173900300

Pull #2304

github

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

135 of 408 new or added lines in 22 files covered. (33.09%)

181 existing lines in 15 files now uncovered.

125892 of 298079 relevant lines covered (42.23%)

117687.12 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