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

ossia / score / 35310244323

18 Sep 2026 05:17AM UTC coverage: 39.902% (+2.2%) from 37.724%
35310244323

push

github

jcelerier
tests: include CommandDispatcher where JsLibraryScriptSavingTest uses it

ee4b67af6f added the test with a CommandDispatcher<> but without its header,
relying on it arriving transitively. The Coverage build compiles with clang-19
at gnu++23 and does not get it that way:

  JsLibraryScriptSavingTest.cpp:70:3: error: no template named 'CommandDispatcher'

This has been broken since 2026-09-13 and was hidden behind the boost download
failure, which stopped that job before it ever reached the compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1e2ZdUQh7Srmv6BGVakrQ

114906 of 287968 relevant lines covered (39.9%)

77906.02 hits per line

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

65.51
/src/plugins/score-plugin-js/JS/JSProcessModel.cpp
1
// This is an open source non-commercial project. Dear PVS-Studio, please check
2
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3

4
#include "JSProcessModel.hpp"
5

6
#include <State/Expression.hpp>
7

8
#include <Process/Dataflow/Port.hpp>
9
#include <Process/ExternalFiles.hpp>
10
#include <Process/PresetHelpers.hpp>
11

12
#include <JS/ApplicationPlugin.hpp>
13
#include <JS/Commands/EditScript.hpp>
14
#include <JS/Executor/ExecutionHelpers.hpp>
15
#include <JS/JSProcessMetadata.hpp>
16
#include <JS/Qml/QmlObjects.hpp>
17
#include <Library/LibrarySettings.hpp>
18

19
#include <score/application/GUIApplicationContext.hpp>
20
#include <score/command/Dispatchers/MultiOngoingCommandDispatcher.hpp>
21
#include <score/command/Dispatchers/SingleOngoingCommandDispatcher.hpp>
22
#include <score/document/DocumentInterface.hpp>
23
#include <score/model/Identifier.hpp>
24
#include <score/serialization/VisitorCommon.hpp>
25
#include <score/tools/DeleteAll.hpp>
26
#include <score/tools/File.hpp>
27

28
#include <core/document/Document.hpp>
29

30
#include <QDebug>
31
#include <QDir>
32
#include <QFileInfo>
33
#include <QFileSystemWatcher>
34
#include <QGuiApplication>
35
#include <QPointer>
36
#include <QQmlComponent>
37
#include <QQuickItem>
38
#include <QQuickWindow>
39
#include <QStandardPaths>
40

41
#include <wobjectimpl.h>
42

43
#include <vector>
44
W_OBJECT_IMPL(JS::ProcessModel)
4,016✔
45
namespace JS
46
{
47
static constexpr const char* default_js_program =
48
    R"_(import Score
49
import QtQuick
50
// This is a minimal example script that showcases the available API.
51
// View the complete documentation at
52
// https://ossia.io/score-docs/processes/javascript.html
53
Script {
54
  ValueInlet { id: in1; objectName: "Value In" }
55
  ValueOutlet { id: out1; objectName: "Value Out" }
56
  FloatSlider { id: sl; min: 10; max: 100; objectName: "Control" }
57

58
  // Called on every tick
59
  tick: function(token, state)
60
  {
61
    if (typeof in1.value !== 'undefined')
62
    {
63
      console.log(in1.value);
64
      out1.value = in1.value * mx + sl.value * my;
65
    }
66
  }
67
})_";
68
static constexpr const char* default_js_ui =
69
    R"_()_";
70

71
ProcessModel::ProcessModel(
49✔
72
    const TimeVal& duration, const QString& data, const Id<Process::ProcessModel>& id,
73
    QObject* parent)
74
    : Process::ProcessModel{
49✔
75
        duration, id, Metadata<ObjectKey_k, ProcessModel>::get(), parent}
49✔
76
{
49✔
77
  if(data.isEmpty())
49✔
78
  {
79
    (void)setProgram({default_js_program, default_js_ui});
36✔
80
  }
36✔
81
  else
82
  {
83
    if(!data.endsWith(".qml")) {
13✔
84
      (void)setProgram({data, {}});
1✔
85
    }
1✔
86
    else {
87
      m_root = data;
12✔
88
      (void)setProgram(readProgramFromFile(data));
12✔
89
    }
90
  }
91

92
  metadata().setInstanceName(*this);
49✔
93
}
×
94

95
Process::ProcessFlags ProcessModel::flags() const noexcept
1,342✔
96
{
97
  auto flags = Metadata<Process::ProcessFlags_k, JS::ProcessModel>::get();
1,342✔
98
  if(m_ui_component)
1,342✔
99
    // The UI is a Qt Quick scene in a window container: it can be docked.
100
    flags |= Process::ExternalUIAvailable | Process::ExternalUIEmbeddable;
200✔
101
  return flags;
1,342✔
102
}
103

104
ProcessModel::~ProcessModel()
114✔
105
{
57✔
106
  if(this->externalUI)
57✔
107
  {
108
    this->externalUI->close();
1✔
109
    this->externalUI = nullptr;
1✔
110
  }
1✔
111
}
114✔
112

113
void ProcessModel::mapExternalFiles(Process::ExternalFileMap& map)
6✔
114
{
115
  Process::ProcessModel::mapExternalFiles(map);
6✔
116

117
  auto& ctx = score::IDocument::documentContext(*this);
6✔
118

119
  // The QML import root. Relocating it means collecting a whole include tree,
120
  // which score does not attempt: report it so the user knows the other
121
  // machine needs it.
122
  if(!m_root.isEmpty())
6✔
123
    map.readOnly(m_root, score::FileKind::Script);
3✔
124

125
  // A script is stored either inline or as a path to a .qml file; only the
126
  // latter is an external dependency.
127
  QmlSource next = m_program;
6✔
128
  bool changed = false;
6✔
129
  const auto relocate = [&](QString& script) {
18✔
130
    if(!Process::looksLikeExistingFile(script, ctx))
12✔
131
      return;
12✔
132

133
    const QString relocated = map.map(
×
134
        {.path = script,
×
135
         .kind = score::FileKind::Script,
136
         .usage = Process::FileUsage::Input,
137
         .directory = false,
138
         .rewritable = true,
139
         .owner = map.owner});
×
140
    if(relocated.isEmpty())
×
141
      return;
×
142

143
    script = relocated;
×
144
    changed = true;
×
145
  };
12✔
146
  relocate(next.execution);
6✔
147
  relocate(next.ui);
6✔
148

149
  if(changed)
6✔
150
    map.addCommand(new JS::EditScript{*this, next, ctx});
×
151
}
6✔
152

153
QString ProcessModel::uiPathFor(const QString& qmlPath) noexcept
35✔
154
{
155
  if(!qmlPath.endsWith(".qml", Qt::CaseInsensitive))
35✔
156
    return {};
×
157
  return qmlPath.chopped(3) + QStringLiteral("ui.qml");
35✔
158
}
35✔
159

160
QmlSource ProcessModel::readProgramFromFile(const QString& qmlPath) noexcept
35✔
161
{
162
  const auto read = [](const QString& path) -> QString {
105✔
163
    if(path.isEmpty())
70✔
164
      return {};
×
165
    QFile f{path};
70✔
166
    if(!f.open(QIODevice::ReadOnly))
70✔
167
      return {};
19✔
168
    return QString::fromUtf8(f.readAll());
51✔
169
  };
70✔
170
  return {read(qmlPath), read(uiPathFor(qmlPath))};
35✔
171
}
172

173
void ProcessModel::updateFileLink() noexcept
85✔
174
{
175
  m_modified = m_root.isEmpty() || m_program != readProgramFromFile(m_root);
85✔
176
}
85✔
177

178
QString ProcessModel::rootPath() const noexcept
97✔
179
{
180
  if(!m_root.isEmpty())
97✔
181
  {
182
    return m_root;
30✔
183
  }
184
  else
185
  {
186
    // Not cached: tests run several applications in one process
187
    const auto& lib = score::AppContext().settings<Library::Settings::Model>();
67✔
188

189
    return lib.getDefaultLibraryPath() + QDir::separator() + "Scripts"
134✔
190
           + QDir::separator() + "include" + QDir::separator() + "Script/Script.qml";
67✔
191
  }
192
}
97✔
193

194
bool ProcessModel::validate(const std::vector<QString>& script) const noexcept
6✔
195
{
196
  if(script.empty())
6✔
197
    return false;
×
198
  if(script[0].isEmpty())
6✔
199
    return false;
×
200

201
  const auto trimmed = script[0].trimmed();
6✔
202
  const QByteArray data = trimmed.toUtf8();
6✔
203

204
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
6✔
205

206
  if(QFileInfo::exists(path))
6✔
207
  {
208
    return (bool)m_cache.getExecution(*this, path.toUtf8(), true);
×
209
  }
210
  else
211
  {
212
    if(!data.startsWith("import"))
6✔
213
      return false;
×
214
    return (bool)m_cache.getExecution(*this, data, false);
6✔
215
  }
216
}
6✔
217

218

219
QString ProcessModel::effect() const noexcept
217✔
220
{
221
  return m_qmlData;
217✔
222
}
223

224
QQuickItem* ProcessModel::createItemForUI(const score::DocumentContext& ctx) const noexcept
32✔
225
{
226
  if(!m_ui_component)
32✔
227
    return nullptr;
3✔
228
  auto& dummyEngine =  ctx.app
29✔
229
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
29✔
230
                          .m_scriptProcessUIEngine;
29✔
231

232
  auto obj = m_ui_component->beginCreate(dummyEngine.rootContext());
29✔
233

234
  if(!obj)
29✔
235
    return nullptr;
×
236
  auto script = qobject_cast<ScriptUI*>(obj);
29✔
237
  auto self = const_cast<JS::ProcessModel*>(this);
29✔
238
  if(script) {
29✔
239
    script->setProcess(self);
29✔
240
  }
29✔
241
  m_ui_component->completeCreate();
29✔
242

243
  if(!script) {
29✔
244
    delete obj;
×
245
    return nullptr;
×
246
  }
247

248
  if(const auto& on_exec = script->executionEvent(); on_exec.isCallable())
29✔
249
  {
250
    connect(this, &JS::ProcessModel::executionToUi,
×
251
            script, [&dummyEngine, on_exec] (const QVariant& v) {
×
252
      on_exec.call({dummyEngine.toScriptValue(v)});
×
253
    });
×
254
  }
×
255

256
  connect(script, &ScriptUI::executionSend, this, [self](const QJSValue& v) {
29✔
257
    self->uiToExecution(v.toVariant());
×
258
  });
×
259

260
  struct StateUpdater
261
  {
262
    const score::DocumentContext& ctx;
263
    JS::ProcessModel& self;
264
    std::unique_ptr<MultiOngoingCommandDispatcher> disp;
265
    int count = 0;
266

267
    void beginUpdateState(const QString& name)
×
268
    {
269
      if(count > 0) {
×
270
        count++;
×
271
      }
×
272
      else {
273
        disp = std::make_unique<MultiOngoingCommandDispatcher>(ctx.commandStack);
×
274
        count = 1;
×
275
      }
276
    }
×
277

278
    void endUpdateState()
×
279
    {
280
      if(!disp)
×
281
        return;
×
282
      count--;
×
283
      if(count > 0)
×
284
        return;
×
285

286
      disp->commit<JS::UpdateStateMacro>();
×
287
      disp.reset();
×
288
      count = 0;
×
289
    }
×
290

291
    void updateState(const QString& k, const QJSValue& v)
×
292
    {
293
      beginUpdateState("Update");
×
294

295
      disp->submit<JS::UpdateStateElement>(self, k, ossia::qt::value_from_js(v));
×
296

297
      endUpdateState();
×
298
    }
×
299

300
    void cancelUpdateState()
×
301
    {
302
      if(!disp)
×
303
        return;
×
304

305
      disp->rollback();
×
306
      disp.reset();
×
307
      count = 0;
×
308
    }
×
309

310
    void clearState()
×
311
    {
312
      beginUpdateState("Clear");
×
313

314
      disp->submit<JS::ReplaceState>(self, JS::JSState{});
×
315

316
      endUpdateState();
×
317
    }
×
318

319
    void replaceState(const QJSValue& v)
×
320
    {
321
      beginUpdateState("Replace");
×
322

323
      JS::JSState cur;
×
324
      auto var = v.toVariant().toMap();
×
325
      for(auto it = var.constBegin(); it != var.constEnd(); ++it) {
×
326
        if(it.value().isValid()) {
×
327
          cur.insert_or_assign(it.key(), ossia::qt::qt_to_ossia{}(it.value()));
×
328
        }
×
329
      }
×
330
      disp->submit<JS::ReplaceState>(self, std::move(cur));
×
331

332
      endUpdateState();
×
333
    }
×
334
  };
335
  auto updater = std::make_shared<StateUpdater>(StateUpdater{ctx, *self});
29✔
336

337
  connect(script, &ScriptUI::beginUpdateState,
58✔
338
          this, [updater] (const QString& name) {
29✔
339
    updater->beginUpdateState(name);
×
340
  });
×
341
  connect(script, &ScriptUI::updateState,
58✔
342
          this, [updater] (const QString& name, const QJSValue& v) {
29✔
343
    updater->updateState(name, v);
×
344
  });
×
345
  connect(script, &ScriptUI::endUpdateState,
58✔
346
          this, [updater] () {
29✔
347
    updater->endUpdateState();
×
348
  });
×
349
  connect(script, &ScriptUI::cancelUpdateState,
58✔
350
          this, [updater] () {
29✔
351
    updater->cancelUpdateState();
×
352
  });
×
353
  connect(script, &ScriptUI::clearState,
58✔
354
          this, [updater] () {
29✔
355
    updater->clearState();
×
356
  });
×
357
  connect(script, &ScriptUI::replaceState,
58✔
358
          this, [updater] (const QJSValue& v) {
29✔
359
    updater->replaceState(v);
×
360
  });
×
361

362
  if(const auto& on_stateUpdated = script->stateUpdated(); on_stateUpdated.isCallable())
29✔
363
  {
364
    connect(
×
365
        this, &JS::ProcessModel::stateElementChanged, script,
×
366
        [on_stateUpdated, &dummyEngine](const QString& k, const ossia::value& v) {
×
367
      if(v.valid())
×
368
      {
369
        if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
370
          on_stateUpdated.call({k, dummyEngine.toScriptValue(res)});
×
371
        else
372
          on_stateUpdated.call({k, QJSValue{}});
×
373
      }
×
374
      else
375
        on_stateUpdated.call({k, QJSValue{}});
×
376
    }, Qt::QueuedConnection);
×
377
  }
×
378

379
  if(const auto& on_load = script->loadState(); on_load.isCallable())
29✔
380
  {
381
    QVariantMap vm;
×
382
    for(auto& [k, v]: this->m_state) {
×
383
      if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
384
        vm[k] = std::move(res);
×
385
    }
386
    on_load.call({dummyEngine.toScriptValue(vm)});
×
387
  }
×
388

389
  return script;
29✔
390
}
32✔
391

392
QWidget* ProcessModel::createWindowForUI(const score::DocumentContext& ctx,
23✔
393
                                         QWidget* parent) const noexcept
394
{
395
  m_ui_object = createItemForUI(ctx);
23✔
396
  if(!m_ui_object)
23✔
397
    return nullptr;
×
398

399
  // A close is deferred: by the time this runs the process may show a new
400
  // UI, which must be left alone. The container pointer is only compared,
401
  // the widget may be half-destroyed.
402
  const auto cleanup_ui = [this](QQuickWindow* win, QWidget* container) {
45✔
403
    if(m_ui_object && m_ui_object->window() == win)
22✔
404
    {
405
      delete m_ui_object;
×
406
      m_ui_object = nullptr;
×
407
    }
×
408

409
    if(externalUI == container)
22✔
410
    {
411
      const_cast<QWidget*&>(externalUI) = nullptr;
×
412
      externalUIVisible(false);
×
413
    }
×
414
  };
22✔
415

416
  // Let the UI take the whole window: needed when the window is docked in
417
  // the main window and follows the size of its pane.
418
  const auto fitToWindow = [](QQuickWindow* win, QQuickItem* item) {
50✔
419
    if(win && item)
27✔
420
      item->setSize(QSizeF(win->width(), win->height()));
27✔
421
  };
27✔
422

423
  // On a new compile the UI object is recreated; put it back in the window
424
  const auto reloadUI = [this, &ctx, fitToWindow](QQuickWindow* win) -> bool {
26✔
425
    // A window closed and waiting for its deletion while a new one holds
426
    // the UI: nothing to do in it
427
    if(m_ui_object && m_ui_object->window() != win)
3✔
428
      return true;
×
429

430
    delete m_ui_object;
3✔
431
    m_ui_object = nullptr;
3✔
432
    if(!m_ui_component)
3✔
433
      return false;
×
434

435
    m_ui_object = createItemForUI(ctx);
3✔
436
    if(!m_ui_object)
3✔
437
      return false;
×
438
    m_ui_object->setParentItem(win->contentItem());
3✔
439
    m_ui_object->setParent(win->contentItem());
3✔
440
    fitToWindow(win, m_ui_object);
3✔
441
    return true;
3✔
442
  };
3✔
443

444
  // The requested size: the ScriptUI root asks for one through its implicit
445
  // width / height (e.g. `implicitWidth: 1280`), defaulting to 640x640.
446
  const QSize requested = [this] {
46✔
447
    int w = 640, h = 640;
23✔
448
    if(m_ui_object->implicitWidth() >= 100.)
23✔
449
      w = static_cast<int>(m_ui_object->implicitWidth());
20✔
450
    if(m_ui_object->implicitHeight() >= 100.)
23✔
451
      h = static_cast<int>(m_ui_object->implicitHeight());
20✔
452
    return QSize{w, h};
23✔
453
  }();
454

455
  auto win = new QQuickWindow{};
23✔
456
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
457
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
458
  // leaves the window with no title bar, close or minimise button.
459
  win->setFlags(
23✔
460
      win->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
23✔
461
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
23✔
462
      | Qt::WindowMaximizeButtonHint);
23✔
463
#if defined(__EMSCRIPTEN__)
464
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
465
  // turns into showFullScreen() for every top level: the requested size is
466
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
467
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
468
  win->setFlags(win->flags() | Qt::Dialog);
469
#endif
470
  win->setWidth(640);
23✔
471
  win->setHeight(640);
23✔
472
  win->setColor(qApp->palette().color(QPalette::Window));
23✔
473

474
  m_ui_object->setParentItem(win->contentItem());
23✔
475
  connect(win, &QQuickWindow::widthChanged, this, [this, win, fitToWindow] {
35✔
476
    fitToWindow(win, m_ui_object);
12✔
477
  });
12✔
478
  connect(win, &QQuickWindow::heightChanged, this, [this, win, fitToWindow] {
35✔
479
    fitToWindow(win, m_ui_object);
12✔
480
  });
12✔
481

482
  auto widg = QWidget::createWindowContainer(win, parent);
23✔
483
  if(!widg) {
23✔
484
    delete m_ui_object;
×
485
    m_ui_object = nullptr;
×
486
    delete win;
×
487
    return nullptr;
×
488
  }
489
  widg->setAttribute(Qt::WA_DeleteOnClose);
23✔
490
  // The container widget does not follow the QQuickWindow's size
491
  widg->resize(requested);
23✔
492

493
#if QT_VERSION >= QT_VERSION_CHECK(6,8,2)
494
  // Bug in older Qt 6 versions:
495
  // QtCore/qmetatype.h:842:23: error: invalid application of 'sizeof' to an incomplete type 'QQuickCloseEvent'
496
  // static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");
497
  connect(win, &QQuickWindow::closing, this, [cleanup_ui, win, widg] {
498
    cleanup_ui(win, widg);
499
  });
500
#endif
501
  connect(win, &QQuickWindow::destroyed, this, [cleanup_ui, win, widg] {
45✔
502
    cleanup_ui(win, widg);
22✔
503
  });
22✔
504
  connect(
23✔
505
      this, &JS::ProcessModel::uiScriptOk, win,
23✔
506
      [reloadUI, win, container = QPointer<QWidget>{widg}] {
26✔
507
    if(!reloadUI(win))
3✔
508
    {
509
      // The container is what the main window holds: closing it takes the
510
      // window with it and runs the cleanup from destroyed()
511
      if(container)
×
512
        container->close();
×
513
    }
×
514
  });
3✔
515

516
  // No externalUIVisible(true) here: setupExternalUI emits it for every
517
  // process type once the UI is placed, and only when one is actually created
518
  // -- asking again for an open UI raises it instead. Emitting a second time
519
  // from this path would make a re-request report {true, true, false}.
520
  return widg;
23✔
521
}
23✔
522

523
void ProcessModel::setExecutionScript(const QString& f)
85✔
524
{
525
  if(f == m_program.execution)
85✔
526
    return;
13✔
527
  m_program.execution = std::move(f);
72✔
528

529
  executionScriptChanged(m_program.execution);
72✔
530
}
85✔
531

532
void ProcessModel::setUiScript(const QString& f)
85✔
533
{
534
  if(f == m_program.ui)
85✔
535
    return;
61✔
536
  m_program.ui = std::move(f);
24✔
537

538
  uiScriptChanged(m_program.ui);
24✔
539
}
85✔
540

541
void ProcessModel::setState(const JSState &s)
8✔
542
{
543
  if(s == m_state)
8✔
544
    return;
8✔
545

546
  {
547
    const auto prev = std::move(m_state);
×
548
    for(auto& [prev_k, prev_v] : prev) {
×
549
      stateElementChanged(prev_k, prev_v);
×
550
    }
551
  }
×
552

553
  m_state = std::move(s);
×
554
  for(auto& [k, v] : m_state) {
×
555
    stateElementChanged(k, v);
×
556
  }
557

558
  stateChanged();
×
559
}
8✔
560

561
void ProcessModel::updateState(const QString &k, const ossia::value& res)
×
562
{
563
  if(auto it = m_state.find(k); it != m_state.end())
×
564
  {
565
    if(res.valid())
×
566
    {
567
      if(res != it->second)
×
568
      {
569
        // Updating a new element
570
        m_state[k] = res;
×
571
        stateElementChanged(k, res);
×
572
        stateChanged();
×
573
      }
×
574
    }
×
575
    else
576
    {
577
      // Removing an element
578
      m_state.erase(k);
×
579
      stateElementChanged(k, res);
×
580
      stateChanged();
×
581
    }
582
  }
×
583
  else
584
  {
585
    if(res.valid())
×
586
    {
587
      // Adding a new element
588
      m_state[k] = res;
×
589
      stateElementChanged(k, res);
×
590
      stateChanged();
×
591
    }
×
592
    else
593
    {
594
      // Already not there, nothing to do
595
    }
596
  }
597
}
×
598

599
[[nodiscard]] Process::ScriptChangeResult ProcessModel::setProgram(const JS::QmlSource& script)
85✔
600
{
601
  setExecutionScript(script.execution);
85✔
602
  setUiScript(script.ui);
85✔
603

604
  Process::ScriptChangeResult res;
85✔
605
  const auto trimmed = script.execution.trimmed();
85✔
606
  const QByteArray data = trimmed.toUtf8();
85✔
607

608
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
85✔
609

610
  // setQmlData builds the UI component from m_program.ui: it must be the new
611
  // one, otherwise every compile shows the UI of the previous compile.
612
  const auto previous = m_program;
85✔
613
  m_program = script;
85✔
614

615
  if(QFileInfo::exists(path))
85✔
616
    res = setQmlData(path.toUtf8(), true);
×
617
  else
618
    res = setQmlData(data, false);
85✔
619

620
  if(!res.valid)
85✔
621
    m_program = previous;
3✔
622
  updateFileLink();
85✔
623
  return res;
85✔
624
}
85✔
625

626
Process::ScriptChangeResult ProcessModel::setQmlData(const QByteArray& data, bool isFile)
85✔
627
{
628
  Process::ScriptChangeResult res;
85✔
629
  if(!isFile && !data.contains("import "))
85✔
630
    return res;
×
631

632
  auto script = m_cache.getExecution(*this, data, isFile);
85✔
633
  if(!script)
85✔
634
    return res;
3✔
635

636
  m_isFile = isFile;
82✔
637
  m_qmlData = data;
82✔
638

639
  res.inlets = score::clearAndDeleteLater(m_inlets);
82✔
640
  res.outlets = score::clearAndDeleteLater(m_outlets);
82✔
641
  const bool had_ui = m_ui_component;
82✔
642
  m_ui_component = nullptr;
82✔
643
  delete m_ui_object;
82✔
644
  m_ui_object = nullptr;
82✔
645

646
  SCORE_ASSERT(m_inlets.size() == 0);
82✔
647
  SCORE_ASSERT(m_outlets.size() == 0);
82✔
648

649
  // Check inlets / outlets
650
  {
651
    auto cld_inlet = script->findChildren<Inlet*>();
82✔
652
    int i = 0;
82✔
653
    for(auto n : cld_inlet)
205✔
654
    {
655
      auto port = n->make(Id<Process::Port>(i++), this);
123✔
656
      if(const auto& name = n->objectName(); !name.isEmpty())
219✔
657
        port->setName(name);
96✔
658
      if(auto addr = State::parseAddressAccessor(n->address()))
123✔
659
        port->setAddress(std::move(*addr));
×
660
      m_inlets.push_back(port);
123✔
661
    }
662
  }
82✔
663

664
  {
665
    auto cld_outlet = script->findChildren<Outlet*>();
82✔
666
    int i = 0;
82✔
667
    for(auto n : cld_outlet)
160✔
668
    {
669
      auto port = n->make(Id<Process::Port>(i++), this);
78✔
670
      if(const auto& name = n->objectName(); !name.isEmpty())
133✔
671
        port->setName(name);
55✔
672
      if(auto addr = State::parseAddressAccessor(n->address()))
78✔
673
        port->setAddress(std::move(*addr));
×
674
      m_outlets.push_back(port);
78✔
675
    }
676
  }
82✔
677

678
  // Create ui if any
679
  if(!this->m_program.ui.isEmpty()) {
82✔
680
    m_ui_component = m_cache.getUi(*this, this->m_program.ui.toUtf8(), isFile);
27✔
681
  }
27✔
682

683
  if(m_isFile)
82✔
684
  {
685
    const auto name = QFileInfo{data}.baseName();
×
686
    metadata().setName(name);
×
687
    metadata().setLabel(name);
×
688
  }
×
689
  else if(metadata().getName().isEmpty())
82✔
690
  {
691
    metadata().setName(QStringLiteral("Script"));
47✔
692
  }
47✔
693

694
  executionScriptOk();
82✔
695
  res.valid = true;
82✔
696

697
  if(bool(m_ui_component) != had_ui)
82✔
698
    flagsChanged();
9✔
699

700
  if(m_ui_component)
82✔
701
  {
702
    uiScriptOk();
11✔
703
  }
11✔
704
  else if(externalUI)
71✔
705
  {
706
    externalUI->close();
×
707
    externalUI->deleteLater();
×
708
    externalUI = nullptr;
×
709
    externalUIVisible(false);
×
710
  }
×
711

712
  // inlets / outletsChanged : in ScriptEditCommand
713
  return res;
82✔
714
}
85✔
715

716
Script* ProcessModel::currentExecutionObject() const noexcept
4✔
717
{
718
  if(auto cache = m_cache.tryGet(m_qmlData, m_isFile))
4✔
719
    return cache->object.get();
2✔
720
  return nullptr;
2✔
721
}
4✔
722

723
bool ProcessModel::isGpu() const noexcept
4✔
724
{
725
#if defined(SCORE_HAS_GPU_JS)
726
  if(auto script = currentExecutionObject())
727
  {
728
    return
729
        script->findChild<JS::TextureInlet*>() != nullptr
730
           || script->findChild<JS::TextureOutlet*>() != nullptr
731
           // || script->findChild<JS::BufferInlet*>() != nullptr
732
           // || script->findChild<JS::BufferOutlet*>() != nullptr
733
        ;
734
  }
735
#endif
736
  return false;
4✔
737
}
738

739
ComponentCache::ComponentCache() { }
57✔
740
ComponentCache::~ComponentCache() { }
57✔
741

742
QByteArray ComponentCache::key(const QByteArray& str, bool isFile) noexcept
199✔
743
{
744
  if(!isFile)
199✔
745
    return str;
199✔
746

747
  // What is cached is a compiled script, so what identifies it is the text it
748
  // was compiled from -- not the name of the file that happened to hold it,
749
  // which says nothing about whether that file still says the same thing.
750
  QFile f{QString::fromUtf8(str)};
×
751
  if(!f.open(QIODevice::ReadOnly))
×
752
    return {};
×
753
  return f.readAll();
×
754
}
199✔
755

756
const ComponentCache::Cache* ComponentCache::tryGet(const QByteArray& str, bool isFile) const noexcept
122✔
757
{
758
  const QByteArray content = key(str, isFile);
122✔
759
  if(content.isEmpty())
122✔
760
    return nullptr;
2✔
761

762
  if(auto it = ossia::find_if(m_map, [&](const auto& k) { return k.key == content; });
214✔
763
     it != m_map.end())
120✔
764
  {
765
    return &*it;
24✔
766
  }
767
  return nullptr;
96✔
768
}
122✔
769

770
Script* ComponentCache::getExecution(
91✔
771
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
772
{
773
  if(auto cache = tryGet(str, isFile))
91✔
774
    return cache->object.get();
20✔
775

776
  auto& dummyEngine = score::GUIAppContext()
142✔
777
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
71✔
778
                          .m_scriptProcessUIEngine;
71✔
779
  std::unique_ptr<QQmlComponent> comp;
71✔
780
  if(!isFile)
71✔
781
  {
782
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
71✔
783
    loadJSObjectFromString(process.rootPath(), str, *comp, false);
71✔
784
  }
71✔
785
  else
786
  {
787
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
788
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
789
  }
790

791
  const auto& errs = comp->errors();
71✔
792
  if(!errs.empty())
71✔
793
  {
794
    const auto& err = errs.first();
3✔
795
    qDebug() << err.line() << err.toString();
3✔
796
    auto str = err.toString();
3✔
797
    str.remove("<Unknown File>:");
3✔
798
    process.errorMessage(/* err.line(), */str);
3✔
799
    return nullptr;
3✔
800
  }
3✔
801

802
  auto obj = comp->create();
68✔
803
  auto script = qobject_cast<JS::Script*>(obj);
68✔
804
  if(script)
68✔
805
  {
806
    if(m_map.size() > 5)
68✔
807
      m_map.erase(m_map.begin());
×
808

809
    m_map.emplace_back(
136✔
810
        Cache{key(str, isFile), std::move(comp), std::unique_ptr<JS::Script>(script)});
68✔
811
    return script;
68✔
812
  }
813
  else
814
  {
815
    process.errorMessage(/* 0, */"The component must be of type Script");
×
816
    if(obj)
×
817
    {
818
      delete obj;
×
819
    }
×
820
    return nullptr;
×
821
  }
822
}
91✔
823

824
QQmlComponent* ComponentCache::getUi(
27✔
825
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
826
{
827
  if(auto cache = tryGet(str, isFile))
27✔
828
    return cache->component.get();
2✔
829

830
  auto& dummyEngine = score::GUIAppContext()
50✔
831
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
25✔
832
                          .m_scriptProcessUIEngine;
25✔
833

834
  std::unique_ptr<QQmlComponent> comp;
25✔
835
  if(!isFile)
25✔
836
  {
837
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
25✔
838
    loadJSObjectFromString(process.rootPath(), str, *comp, true);
25✔
839
  }
25✔
840
  else
841
  {
842
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
843
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
844
  }
845

846
  const auto& errs = comp->errors();
25✔
847
  if(!errs.empty())
25✔
848
  {
849
    const auto& err = errs.first();
9✔
850
    qDebug() << err.line() << err.toString();
9✔
851
    auto str = err.toString();
9✔
852
    str.remove("<Unknown File>:");
9✔
853
    process.errorMessage(/* err.line(), */str);
9✔
854
    return nullptr;
9✔
855
  }
9✔
856

857
  auto obj = comp->beginCreate(dummyEngine.rootContext());
16✔
858
  if(!obj) {
16✔
859
    process.errorMessage(/* 0, */"Cannot create UI object");
×
860
    return nullptr;
×
861
  }
862
  auto script = qobject_cast<ScriptUI*>(obj);
16✔
863
  if(script) {
16✔
864
    script->setProcess((Process::ProcessModel*)&process);
9✔
865
  }
9✔
866
  comp->completeCreate();
16✔
867
  if(script)
16✔
868
  {
869
    if(m_map.size() > 5)
9✔
870
      m_map.erase(m_map.begin());
×
871

872
    m_map.emplace_back(
18✔
873
        Cache{key(str, isFile), std::move(comp), {}});
9✔
874
    delete script;
9✔
875
    return m_map.back().component.get();
9✔
876
  }
877
  else
878
  {
879
    process.errorMessage(/* 0, */"The component must be of type Script");
7✔
880
    if(obj)
7✔
881
      delete obj;
7✔
882
    return nullptr;
7✔
883
  }
884
}
27✔
885

886
void ProcessModel::loadPreset(const Process::Preset& preset)
×
887
{
888
  Process::loadScriptProcessPreset<ProcessModel::p_program>(*this, preset);
×
889
}
×
890

891
Process::Preset ProcessModel::savePreset() const noexcept
×
892
{
893
  // FIXME this should save p_program
894
  return Process::saveScriptProcessPreset(*this, this->m_qmlData);
×
895
}
896

897
}
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