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

ossia / score / 36105248298

25 Sep 2026 06:57AM UTC coverage: 42.611% (+0.4%) from 42.23%
36105248298

Pull #2308

github

web-flow
Merge 07da0e711 into c0c64ec39
Pull Request #2308: tests: a green test suite, and the sanitizer findings in score fixed

511 of 687 new or added lines in 50 files covered. (74.38%)

184 existing lines in 23 files now uncovered.

127081 of 298233 relevant lines covered (42.61%)

118531.04 hits per line

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

71.62
/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/JSONVisitor.hpp>
25
#include <score/serialization/VisitorCommon.hpp>
26
#include <score/tools/DeleteAll.hpp>
27
#include <score/tools/File.hpp>
28

29
#include <optional>
30

31
#include <core/document/Document.hpp>
32

33
#include <QDebug>
34
#include <QDir>
35
#include <QFileInfo>
36
#include <QFileSystemWatcher>
37
#include <QGuiApplication>
38
#include <QPointer>
39
#include <QQmlComponent>
40
#include <QQuickItem>
41
#include <QQuickWindow>
42
#include <QStandardPaths>
43

44
#include <wobjectimpl.h>
45

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

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

75
ProcessModel::ProcessModel(
67 ✔
76
    const TimeVal& duration, const QString& data, const Id<Process::ProcessModel>& id,
77
    QObject* parent)
78
    : Process::ProcessModel{
67 ✔
79
        duration, id, Metadata<ObjectKey_k, ProcessModel>::get(), parent}
67 ✔
80
{
67 ✔
81
  if(data.isEmpty())
67 ✔
82
  {
83
    (void)setProgram({default_js_program, default_js_ui});
48 ✔
84
  }
48 ✔
85
  else
86
  {
87
    if(!data.endsWith(".qml")) {
19 ✔
88
      (void)setProgram({data, {}});
1 ✔
89
    }
1 ✔
90
    else {
91
      m_root = data;
18 ✔
92
      (void)setProgram(readProgramFromFile(data));
18 ✔
93
    }
94
  }
95

96
  metadata().setInstanceName(*this);
67 ✔
97
}
×
98

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

108
ProcessModel::~ProcessModel()
154 ✔
109
{
77 ✔
110
  if(this->externalUI)
77 ✔
111
  {
112
    this->externalUI->close();
1 ✔
113
    this->externalUI = nullptr;
1 ✔
114
  }
1 ✔
115
}
154 ✔
116

117
void ProcessModel::mapExternalFiles(Process::ExternalFileMap& map)
8 ✔
118
{
119
  Process::ProcessModel::mapExternalFiles(map);
8 ✔
120

121
  auto& ctx = score::IDocument::documentContext(*this);
8 ✔
122

123
  // The QML import root. Relocating it means collecting a whole include tree,
124
  // which score does not attempt: report it so the user knows the other
125
  // machine needs it.
126
  if(!m_root.isEmpty())
8 ✔
127
    map.readOnly(m_root, score::FileKind::Script);
5 ✔
128

129
  // A script is stored either inline or as a path to a .qml file; only the
130
  // latter is an external dependency.
131
  QmlSource next = m_program;
8 ✔
132
  bool changed = false;
8 ✔
133
  const auto relocate = [&](QString& script) {
24 ✔
134
    if(!Process::looksLikeExistingFile(script, ctx))
16 ✔
135
      return;
16 ✔
136

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

147
    script = relocated;
×
148
    changed = true;
×
149
  };
16 ✔
150
  relocate(next.execution);
8 ✔
151
  relocate(next.ui);
8 ✔
152

153
  if(changed)
8 ✔
154
    map.addCommand(new JS::EditScript{*this, next, ctx});
×
155
}
8 ✔
156

157
QString ProcessModel::uiPathFor(const QString& qmlPath) noexcept
58 ✔
158
{
159
  if(!qmlPath.endsWith(".qml", Qt::CaseInsensitive))
58 ✔
160
    return {};
×
161
  return qmlPath.chopped(3) + QStringLiteral("ui.qml");
58 ✔
162
}
58 ✔
163

164
QmlSource ProcessModel::readProgramFromFile(const QString& qmlPath) noexcept
58 ✔
165
{
166
  const auto read = [](const QString& path) -> QString {
174 ✔
167
    if(path.isEmpty())
116 ✔
168
      return {};
×
169
    QFile f{path};
116 ✔
170
    if(!f.open(QIODevice::ReadOnly))
116 ✔
171
      return {};
28 ✔
172
    return QString::fromUtf8(f.readAll());
88 ✔
173
  };
116 ✔
174
  return {read(qmlPath), read(uiPathFor(qmlPath))};
58 ✔
175
}
176

177
void ProcessModel::updateFileLink() noexcept
132 ✔
178
{
179
  const auto file = m_root.isEmpty() ? QmlSource{} : readProgramFromFile(m_root);
132 ✔
180
  m_modified = m_root.isEmpty() || m_program != file;
132 ✔
181

182
  // An unreadable file has nothing to offer: leave the copy the document holds
183
  // alone rather than proposing to replace it with nothing.
184
  const bool outOfDate = m_modified && !file.execution.isEmpty();
132 ✔
185
  if(outOfDate != m_outOfDate)
132 ✔
186
  {
187
    m_outOfDate = outOfDate;
13 ✔
188
    externalSourceOutOfDateChanged(outOfDate);
13 ✔
189
  }
13 ✔
190
}
132 ✔
191

192
score::Command*
193
ProcessModel::refreshFromExternalSource(const score::DocumentContext& ctx) const
4 ✔
194
{
195
  if(!m_outOfDate)
4 ✔
196
    return nullptr;
3 ✔
197

198
  auto next = readProgramFromFile(m_root);
1 ✔
199
  if(next.execution.isEmpty())
1 ✔
200
    return nullptr;
×
201

202
  return new JS::EditScript{*this, std::move(next), ctx};
1 ✔
203
}
4 ✔
204

205
QString ProcessModel::rootPath() const noexcept
148 ✔
206
{
207
  if(!m_root.isEmpty())
148 ✔
208
  {
209
    return m_root;
54 ✔
210
  }
211
  else
212
  {
213
    // Not cached: tests run several applications in one process
214
    const auto& lib = score::AppContext().settings<Library::Settings::Model>();
94 ✔
215

216
    return lib.getDefaultLibraryPath() + QDir::separator() + "Scripts"
188 ✔
217
           + QDir::separator() + "include" + QDir::separator() + "Script/Script.qml";
94 ✔
218
  }
219
}
148 ✔
220

221
bool ProcessModel::validate(const std::vector<QString>& script) const noexcept
6 ✔
222
{
223
  if(script.empty())
6 ✔
224
    return false;
×
225
  if(script[0].isEmpty())
6 ✔
226
    return false;
×
227

228
  const auto trimmed = script[0].trimmed();
6 ✔
229
  const QByteArray data = trimmed.toUtf8();
6 ✔
230

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

233
  if(QFileInfo::exists(path))
6 ✔
234
  {
235
    return (bool)m_cache.getExecution(*this, path.toUtf8(), true);
×
236
  }
237
  else
238
  {
239
    if(!data.startsWith("import"))
6 ✔
240
      return false;
×
241
    return (bool)m_cache.getExecution(*this, data, false);
6 ✔
242
  }
243
}
6 ✔
244

245

246
// A script loaded from a .qml file is identified by that file path;
247
// otherwise by its text
248
QString ProcessModel::effect() const noexcept
312 ✔
249
{
250
  if(!m_root.isEmpty())
312 ✔
251
    return m_root;
99 ✔
252
  return m_qmlData;
213 ✔
253
}
312 ✔
254

255
QQuickItem* ProcessModel::createItemForUI(const score::DocumentContext& ctx) const noexcept
32 ✔
256
{
257
  if(!m_ui_component)
32 ✔
258
    return nullptr;
3 ✔
259
  auto& dummyEngine =  ctx.app
29 ✔
260
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
29 ✔
261
                          .m_scriptProcessUIEngine;
29 ✔
262

263
  auto obj = m_ui_component->beginCreate(dummyEngine.rootContext());
29 ✔
264

265
  if(!obj)
29 ✔
266
    return nullptr;
×
267
  auto script = qobject_cast<ScriptUI*>(obj);
29 ✔
268
  auto self = const_cast<JS::ProcessModel*>(this);
29 ✔
269
  if(script) {
29 ✔
270
    script->setProcess(self);
29 ✔
271
  }
29 ✔
272
  m_ui_component->completeCreate();
29 ✔
273

274
  if(!script) {
29 ✔
275
    delete obj;
×
276
    return nullptr;
×
277
  }
278

279
  if(const auto& on_exec = script->executionEvent(); on_exec.isCallable())
29 ✔
280
  {
281
    connect(this, &JS::ProcessModel::executionToUi,
×
282
            script, [&dummyEngine, on_exec] (const QVariant& v) {
×
NEW
283
      on_exec.call({JS::variantToJs(dummyEngine, v)});
×
284
    });
×
285
  }
×
286

287
  connect(script, &ScriptUI::executionSend, this, [self](const QJSValue& v) {
29 ✔
288
    self->uiToExecution(v.toVariant());
×
289
  });
×
290

291
  struct StateUpdater
292
  {
293
    const score::DocumentContext& ctx;
294
    JS::ProcessModel& self;
295
    std::unique_ptr<MultiOngoingCommandDispatcher> disp;
296
    int count = 0;
297

298
    void beginUpdateState(const QString& name)
×
299
    {
300
      if(count > 0) {
×
301
        count++;
×
302
      }
×
303
      else {
304
        disp = std::make_unique<MultiOngoingCommandDispatcher>(ctx.commandStack);
×
305
        count = 1;
×
306
      }
307
    }
×
308

309
    void endUpdateState()
×
310
    {
311
      if(!disp)
×
312
        return;
×
313
      count--;
×
314
      if(count > 0)
×
315
        return;
×
316

317
      disp->commit<JS::UpdateStateMacro>();
×
318
      disp.reset();
×
319
      count = 0;
×
320
    }
×
321

322
    void updateState(const QString& k, const QJSValue& v)
×
323
    {
324
      beginUpdateState("Update");
×
325

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

328
      endUpdateState();
×
329
    }
×
330

331
    void cancelUpdateState()
×
332
    {
333
      if(!disp)
×
334
        return;
×
335

336
      disp->rollback();
×
337
      disp.reset();
×
338
      count = 0;
×
339
    }
×
340

341
    void clearState()
×
342
    {
343
      beginUpdateState("Clear");
×
344

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

347
      endUpdateState();
×
348
    }
×
349

350
    void replaceState(const QJSValue& v)
×
351
    {
352
      beginUpdateState("Replace");
×
353

354
      JS::JSState cur;
×
355
      auto var = v.toVariant().toMap();
×
356
      for(auto it = var.constBegin(); it != var.constEnd(); ++it) {
×
357
        if(it.value().isValid()) {
×
358
          cur.insert_or_assign(it.key(), ossia::qt::qt_to_ossia{}(it.value()));
×
359
        }
×
360
      }
×
361
      disp->submit<JS::ReplaceState>(self, std::move(cur));
×
362

363
      endUpdateState();
×
364
    }
×
365
  };
366
  auto updater = std::make_shared<StateUpdater>(StateUpdater{ctx, *self});
29 ✔
367

368
  connect(script, &ScriptUI::beginUpdateState,
58 ✔
369
          this, [updater] (const QString& name) {
29 ✔
370
    updater->beginUpdateState(name);
×
371
  });
×
372
  connect(script, &ScriptUI::updateState,
58 ✔
373
          this, [updater] (const QString& name, const QJSValue& v) {
29 ✔
374
    updater->updateState(name, v);
×
375
  });
×
376
  connect(script, &ScriptUI::endUpdateState,
58 ✔
377
          this, [updater] () {
29 ✔
378
    updater->endUpdateState();
×
379
  });
×
380
  connect(script, &ScriptUI::cancelUpdateState,
58 ✔
381
          this, [updater] () {
29 ✔
382
    updater->cancelUpdateState();
×
383
  });
×
384
  connect(script, &ScriptUI::clearState,
58 ✔
385
          this, [updater] () {
29 ✔
386
    updater->clearState();
×
387
  });
×
388
  connect(script, &ScriptUI::replaceState,
58 ✔
389
          this, [updater] (const QJSValue& v) {
29 ✔
390
    updater->replaceState(v);
×
391
  });
×
392

393
  if(const auto& on_stateUpdated = script->stateUpdated(); on_stateUpdated.isCallable())
29 ✔
394
  {
395
    connect(
×
396
        this, &JS::ProcessModel::stateElementChanged, script,
×
397
        [on_stateUpdated, &dummyEngine](const QString& k, const ossia::value& v) {
×
398
      if(v.valid())
×
399
      {
400
        if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
NEW
401
          on_stateUpdated.call({k, JS::variantToJs(dummyEngine, res)});
×
402
        else
403
          on_stateUpdated.call({k, QJSValue{}});
×
404
      }
×
405
      else
406
        on_stateUpdated.call({k, QJSValue{}});
×
407
    }, Qt::QueuedConnection);
×
408
  }
×
409

410
  if(const auto& on_load = script->loadState(); on_load.isCallable())
29 ✔
411
  {
412
    QVariantMap vm;
×
413
    for(auto& [k, v]: this->m_state) {
×
414
      if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
415
        vm[k] = std::move(res);
×
416
    }
NEW
417
    on_load.call({JS::variantToJs(dummyEngine, vm)});
×
418
  }
×
419

420
  return script;
29 ✔
421
}
32 ✔
422

423
QWidget* ProcessModel::createWindowForUI(const score::DocumentContext& ctx,
23 ✔
424
                                         QWidget* parent) const noexcept
425
{
426
  m_ui_object = createItemForUI(ctx);
23 ✔
427
  if(!m_ui_object)
23 ✔
428
    return nullptr;
×
429

430
  // A close is deferred: by the time this runs the process may show a new
431
  // UI, which must be left alone. The container pointer is only compared,
432
  // the widget may be half-destroyed.
433
  const auto cleanup_ui = [this](QQuickWindow* win, QWidget* container) {
45 ✔
434
    if(m_ui_object && m_ui_object->window() == win)
22 ✔
435
    {
436
      delete m_ui_object;
×
437
      m_ui_object = nullptr;
×
438
    }
×
439

440
    if(externalUI == container)
22 ✔
441
    {
442
      const_cast<QWidget*&>(externalUI) = nullptr;
×
443
      externalUIVisible(false);
×
444
    }
×
445
  };
22 ✔
446

447
  // Let the UI take the whole window: needed when the window is docked in
448
  // the main window and follows the size of its pane.
449
  const auto fitToWindow = [](QQuickWindow* win, QQuickItem* item) {
50 ✔
450
    if(win && item)
27 ✔
451
      item->setSize(QSizeF(win->width(), win->height()));
27 ✔
452
  };
27 ✔
453

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

461
    delete m_ui_object;
3 ✔
462
    m_ui_object = nullptr;
3 ✔
463
    if(!m_ui_component)
3 ✔
464
      return false;
×
465

466
    m_ui_object = createItemForUI(ctx);
3 ✔
467
    if(!m_ui_object)
3 ✔
468
      return false;
×
469
    m_ui_object->setParentItem(win->contentItem());
3 ✔
470
    m_ui_object->setParent(win->contentItem());
3 ✔
471
    fitToWindow(win, m_ui_object);
3 ✔
472
    return true;
3 ✔
473
  };
3 ✔
474

475
  // The requested size: the ScriptUI root asks for one through its implicit
476
  // width / height (e.g. `implicitWidth: 1280`), defaulting to 640x640.
477
  const QSize requested = [this] {
46 ✔
478
    int w = 640, h = 640;
23 ✔
479
    if(m_ui_object->implicitWidth() >= 100.)
23 ✔
480
      w = static_cast<int>(m_ui_object->implicitWidth());
20 ✔
481
    if(m_ui_object->implicitHeight() >= 100.)
23 ✔
482
      h = static_cast<int>(m_ui_object->implicitHeight());
20 ✔
483
    return QSize{w, h};
23 ✔
484
  }();
485

486
  auto win = new QQuickWindow{};
23 ✔
487
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
488
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
489
  // leaves the window with no title bar, close or minimise button.
490
  win->setFlags(
23 ✔
491
      win->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
23 ✔
492
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
23 ✔
493
      | Qt::WindowMaximizeButtonHint);
23 ✔
494
#if defined(__EMSCRIPTEN__)
495
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
496
  // turns into showFullScreen() for every top level: the requested size is
497
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
498
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
499
  win->setFlags(win->flags() | Qt::Dialog);
500
#endif
501
  win->setWidth(640);
23 ✔
502
  win->setHeight(640);
23 ✔
503
  win->setColor(qApp->palette().color(QPalette::Window));
23 ✔
504

505
  // The visual parent does not own; the QObject parent makes the window delete
506
  // the UI. cleanup_ui cannot: on destroyed() m_ui_object->window() is null.
507
  m_ui_object->setParentItem(win->contentItem());
23 ✔
508
  m_ui_object->setParent(win->contentItem());
23 ✔
509
  connect(win, &QQuickWindow::widthChanged, this, [this, win, fitToWindow] {
35 ✔
510
    fitToWindow(win, m_ui_object);
12 ✔
511
  });
12 ✔
512
  connect(win, &QQuickWindow::heightChanged, this, [this, win, fitToWindow] {
35 ✔
513
    fitToWindow(win, m_ui_object);
12 ✔
514
  });
12 ✔
515

516
  auto widg = QWidget::createWindowContainer(win, parent);
23 ✔
517
  if(!widg) {
23 ✔
518
    delete m_ui_object;
×
519
    m_ui_object = nullptr;
×
520
    delete win;
×
521
    return nullptr;
×
522
  }
523
  widg->setAttribute(Qt::WA_DeleteOnClose);
23 ✔
524
  // The container widget does not follow the QQuickWindow's size
525
  widg->resize(requested);
23 ✔
526

527
#if QT_VERSION >= QT_VERSION_CHECK(6,8,2)
528
  // Bug in older Qt 6 versions:
529
  // QtCore/qmetatype.h:842:23: error: invalid application of 'sizeof' to an incomplete type 'QQuickCloseEvent'
530
  // static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");
531
  connect(win, &QQuickWindow::closing, this, [cleanup_ui, win, widg] {
532
    cleanup_ui(win, widg);
533
  });
534
#endif
535
  connect(win, &QQuickWindow::destroyed, this, [cleanup_ui, win, widg] {
45 ✔
536
    cleanup_ui(win, widg);
22 ✔
537
  });
22 ✔
538
  connect(
23 ✔
539
      this, &JS::ProcessModel::uiScriptOk, win,
23 ✔
540
      [reloadUI, win, container = QPointer<QWidget>{widg}] {
26 ✔
541
    if(!reloadUI(win))
3 ✔
542
    {
543
      // The container is what the main window holds: closing it takes the
544
      // window with it and runs the cleanup from destroyed()
545
      if(container)
×
546
        container->close();
×
547
    }
×
548
  });
3 ✔
549

550
  // No externalUIVisible(true) here: setupExternalUI emits it for every
551
  // process type once the UI is placed, and only when one is actually created
552
  // -- asking again for an open UI raises it instead. Emitting a second time
553
  // from this path would make a re-request report {true, true, false}.
554
  return widg;
23 ✔
555
}
23 ✔
556

557
void ProcessModel::setExecutionScript(const QString& f)
125 ✔
558
{
559
  if(f == m_program.execution)
125 ✔
560
    return;
13 ✔
561
  m_program.execution = std::move(f);
112 ✔
562

563
  executionScriptChanged(m_program.execution);
112 ✔
564
}
125 ✔
565

566
void ProcessModel::setUiScript(const QString& f)
125 ✔
567
{
568
  if(f == m_program.ui)
125 ✔
569
    return;
92 ✔
570
  m_program.ui = std::move(f);
33 ✔
571

572
  uiScriptChanged(m_program.ui);
33 ✔
573
}
125 ✔
574

575
void ProcessModel::setState(const JSState &s)
31 ✔
576
{
577
  if(s == m_state)
31 ✔
578
    return;
13 ✔
579

580
  {
581
    const auto prev = std::move(m_state);
18 ✔
582
    for(auto& [prev_k, prev_v] : prev) {
35 ✔
583
      stateElementChanged(prev_k, prev_v);
17 ✔
584
    }
585
  }
18 ✔
586

587
  m_state = std::move(s);
18 ✔
588
  for(auto& [k, v] : m_state) {
46 ✔
589
    stateElementChanged(k, v);
28 ✔
590
  }
591

592
  stateChanged();
18 ✔
593
}
31 ✔
594

595
void ProcessModel::updateState(const QString &k, const ossia::value& res)
×
596
{
597
  if(auto it = m_state.find(k); it != m_state.end())
×
598
  {
599
    if(res.valid())
×
600
    {
601
      if(res != it->second)
×
602
      {
603
        // Updating a new element
604
        m_state[k] = res;
×
605
        stateElementChanged(k, res);
×
606
        stateChanged();
×
607
      }
×
608
    }
×
609
    else
610
    {
611
      // Removing an element
612
      m_state.erase(k);
×
613
      stateElementChanged(k, res);
×
614
      stateChanged();
×
615
    }
616
  }
×
617
  else
618
  {
619
    if(res.valid())
×
620
    {
621
      // Adding a new element
622
      m_state[k] = res;
×
623
      stateElementChanged(k, res);
×
624
      stateChanged();
×
625
    }
×
626
    else
627
    {
628
      // Already not there, nothing to do
629
    }
630
  }
631
}
×
632

633
[[nodiscard]] Process::ScriptChangeResult ProcessModel::setProgram(const JS::QmlSource& script)
125 ✔
634
{
635
  setExecutionScript(script.execution);
125 ✔
636
  setUiScript(script.ui);
125 ✔
637

638
  Process::ScriptChangeResult res;
125 ✔
639
  const auto trimmed = script.execution.trimmed();
125 ✔
640
  const QByteArray data = trimmed.toUtf8();
125 ✔
641

642
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
125 ✔
643

644
  // setQmlData builds the UI component from m_program.ui: it must be the new
645
  // one, otherwise every compile shows the UI of the previous compile.
646
  const auto previous = m_program;
125 ✔
647
  m_program = script;
125 ✔
648

649
  if(QFileInfo::exists(path))
125 ✔
650
    res = setQmlData(path.toUtf8(), true);
×
651
  else
652
    res = setQmlData(data, false);
125 ✔
653

654
  if(!res.valid)
125 ✔
655
    m_program = previous;
3 ✔
656
  updateFileLink();
125 ✔
657
  return res;
125 ✔
658
}
125 ✔
659

660
Process::ScriptChangeResult ProcessModel::setQmlData(const QByteArray& data, bool isFile)
125 ✔
661
{
662
  Process::ScriptChangeResult res;
125 ✔
663
  if(!isFile && !data.contains("import "))
125 ✔
664
    return res;
×
665

666
  auto script = m_cache.getExecution(*this, data, isFile);
125 ✔
667
  if(!script)
125 ✔
668
    return res;
3 ✔
669

670
  m_isFile = isFile;
122 ✔
671
  m_qmlData = data;
122 ✔
672

673
  res.inlets = score::clearAndDeleteLater(m_inlets);
122 ✔
674
  res.outlets = score::clearAndDeleteLater(m_outlets);
122 ✔
675
  const bool had_ui = m_ui_component;
122 ✔
676
  m_ui_component = nullptr;
122 ✔
677
  delete m_ui_object;
122 ✔
678
  m_ui_object = nullptr;
122 ✔
679

680
  SCORE_ASSERT(m_inlets.size() == 0);
122 ✔
681
  SCORE_ASSERT(m_outlets.size() == 0);
122 ✔
682

683
  // Check inlets / outlets
684
  {
685
    auto cld_inlet = script->findChildren<Inlet*>();
122 ✔
686
    int i = 0;
122 ✔
687
    for(auto n : cld_inlet)
302 ✔
688
    {
689
      auto port = n->make(Id<Process::Port>(i++), this);
180 ✔
690
      if(const auto& name = n->objectName(); !name.isEmpty())
323 ✔
691
        port->setName(name);
143 ✔
692
      if(auto addr = State::parseAddressAccessor(n->address()))
180 ✔
693
        port->setAddress(std::move(*addr));
×
694
      m_inlets.push_back(port);
180 ✔
695
    }
696
  }
122 ✔
697

698
  {
699
    auto cld_outlet = script->findChildren<Outlet*>();
122 ✔
700
    int i = 0;
122 ✔
701
    for(auto n : cld_outlet)
222 ✔
702
    {
703
      auto port = n->make(Id<Process::Port>(i++), this);
100 ✔
704
      if(const auto& name = n->objectName(); !name.isEmpty())
167 ✔
705
        port->setName(name);
67 ✔
706
      if(auto addr = State::parseAddressAccessor(n->address()))
100 ✔
707
        port->setAddress(std::move(*addr));
×
708
      m_outlets.push_back(port);
100 ✔
709
    }
710
  }
122 ✔
711

712
  // Create ui if any
713
  if(!this->m_program.ui.isEmpty()) {
122 ✔
714
    m_ui_component = m_cache.getUi(*this, this->m_program.ui.toUtf8(), isFile);
39 ✔
715
  }
39 ✔
716

717
  if(m_isFile)
122 ✔
718
  {
719
    const auto name = QFileInfo{data}.baseName();
×
720
    metadata().setName(name);
×
721
    metadata().setLabel(name);
×
722
  }
×
723
  else if(metadata().getName().isEmpty())
122 ✔
724
  {
725
    metadata().setName(QStringLiteral("Script"));
65 ✔
726
  }
65 ✔
727

728
  executionScriptOk();
122 ✔
729
  res.valid = true;
122 ✔
730

731
  if(bool(m_ui_component) != had_ui)
122 ✔
732
    flagsChanged();
9 ✔
733

734
  if(m_ui_component)
122 ✔
735
  {
736
    uiScriptOk();
11 ✔
737
  }
11 ✔
738
  else if(externalUI)
111 ✔
739
  {
740
    externalUI->close();
×
741
    externalUI->deleteLater();
×
742
    externalUI = nullptr;
×
743
    externalUIVisible(false);
×
744
  }
×
745

746
  // inlets / outletsChanged : in ScriptEditCommand
747
  return res;
122 ✔
748
}
125 ✔
749

750
Script* ProcessModel::currentExecutionObject() const noexcept
4 ✔
751
{
752
  if(auto cache = m_cache.tryGet(m_qmlData, m_isFile))
4 ✔
753
    return cache->object.get();
2 ✔
754
  return nullptr;
2 ✔
755
}
4 ✔
756

757
bool ProcessModel::isGpu() const noexcept
4 ✔
758
{
759
#if defined(SCORE_HAS_GPU_JS)
760
  if(auto script = currentExecutionObject())
761
  {
762
    return
763
        script->findChild<JS::TextureInlet*>() != nullptr
764
           || script->findChild<JS::TextureOutlet*>() != nullptr
765
           // || script->findChild<JS::BufferInlet*>() != nullptr
766
           // || script->findChild<JS::BufferOutlet*>() != nullptr
767
        ;
768
  }
769
#endif
770
  return false;
4 ✔
771
}
772

773
ComponentCache::ComponentCache() { }
77 ✔
774
ComponentCache::~ComponentCache() { }
77 ✔
775

776
QByteArray ComponentCache::key(const QByteArray& str, bool isFile) noexcept
288 ✔
777
{
778
  if(!isFile)
288 ✔
779
    return str;
288 ✔
780

781
  // What is cached is a compiled script, so what identifies it is the text it
782
  // was compiled from -- not the name of the file that happened to hold it,
783
  // which says nothing about whether that file still says the same thing.
784
  QFile f{QString::fromUtf8(str)};
×
785
  if(!f.open(QIODevice::ReadOnly))
×
786
    return {};
×
787
  return f.readAll();
×
788
}
288 ✔
789

790
const ComponentCache::Cache* ComponentCache::tryGet(const QByteArray& str, bool isFile) const noexcept
174 ✔
791
{
792
  const QByteArray content = key(str, isFile);
174 ✔
793
  if(content.isEmpty())
174 ✔
794
    return nullptr;
2 ✔
795

796
  if(auto it = ossia::find_if(m_map, [&](const auto& k) { return k.key == content; });
312 ✔
797
     it != m_map.end())
172 ✔
798
  {
799
    return &*it;
27 ✔
800
  }
801
  return nullptr;
145 ✔
802
}
174 ✔
803

804
Script* ComponentCache::getExecution(
131 ✔
805
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
806
{
807
  if(auto cache = tryGet(str, isFile))
131 ✔
808
    return cache->object.get();
23 ✔
809

810
  auto& dummyEngine = score::GUIAppContext()
216 ✔
811
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
108 ✔
812
                          .m_scriptProcessUIEngine;
108 ✔
813
  std::unique_ptr<QQmlComponent> comp;
108 ✔
814
  if(!isFile)
108 ✔
815
  {
816
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
108 ✔
817
    loadJSObjectFromString(process.rootPath(), str, *comp, false);
108 ✔
818
  }
108 ✔
819
  else
820
  {
821
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
822
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
823
  }
824

825
  const auto& errs = comp->errors();
108 ✔
826
  if(!errs.empty())
108 ✔
827
  {
828
    const auto& err = errs.first();
3 ✔
829
    qDebug() << err.line() << err.toString();
3 ✔
830
    auto str = err.toString();
3 ✔
831
    str.remove("<Unknown File>:");
3 ✔
832
    process.errorMessage(/* err.line(), */str);
3 ✔
833
    return nullptr;
3 ✔
834
  }
3 ✔
835

836
  auto obj = comp->create();
105 ✔
837
  auto script = qobject_cast<JS::Script*>(obj);
105 ✔
838
  if(script)
105 ✔
839
  {
840
    if(m_map.size() > 5)
105 ✔
841
      m_map.erase(m_map.begin());
×
842

843
    m_map.emplace_back(
210 ✔
844
        Cache{key(str, isFile), std::move(comp), std::unique_ptr<JS::Script>(script)});
105 ✔
845
    return script;
105 ✔
846
  }
847
  else
848
  {
849
    process.errorMessage(/* 0, */"The component must be of type Script");
×
850
    if(obj)
×
851
    {
852
      delete obj;
×
853
    }
×
854
    return nullptr;
×
855
  }
856
}
131 ✔
857

858
QQmlComponent* ComponentCache::getUi(
39 ✔
859
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
860
{
861
  if(auto cache = tryGet(str, isFile))
39 ✔
862
    return cache->component.get();
2 ✔
863

864
  auto& dummyEngine = score::GUIAppContext()
74 ✔
865
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
37 ✔
866
                          .m_scriptProcessUIEngine;
37 ✔
867

868
  std::unique_ptr<QQmlComponent> comp;
37 ✔
869
  if(!isFile)
37 ✔
870
  {
871
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
37 ✔
872
    loadJSObjectFromString(process.rootPath(), str, *comp, true);
37 ✔
873
  }
37 ✔
874
  else
875
  {
876
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
877
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
878
  }
879

880
  const auto& errs = comp->errors();
37 ✔
881
  if(!errs.empty())
37 ✔
882
  {
883
    const auto& err = errs.first();
11 ✔
884
    qDebug() << err.line() << err.toString();
11 ✔
885
    auto str = err.toString();
11 ✔
886
    str.remove("<Unknown File>:");
11 ✔
887
    process.errorMessage(/* err.line(), */str);
11 ✔
888
    return nullptr;
11 ✔
889
  }
11 ✔
890

891
  auto obj = comp->beginCreate(dummyEngine.rootContext());
26 ✔
892
  if(!obj) {
26 ✔
893
    process.errorMessage(/* 0, */"Cannot create UI object");
×
894
    return nullptr;
×
895
  }
896
  auto script = qobject_cast<ScriptUI*>(obj);
26 ✔
897
  if(script) {
26 ✔
898
    script->setProcess((Process::ProcessModel*)&process);
9 ✔
899
  }
9 ✔
900
  comp->completeCreate();
26 ✔
901
  if(script)
26 ✔
902
  {
903
    if(m_map.size() > 5)
9 ✔
904
      m_map.erase(m_map.begin());
×
905

906
    m_map.emplace_back(
18 ✔
907
        Cache{key(str, isFile), std::move(comp), {}});
9 ✔
908
    delete script;
9 ✔
909
    return m_map.back().component.get();
9 ✔
910
  }
911
  else
912
  {
913
    process.errorMessage(/* 0, */"The component must be of type Script");
17 ✔
914
    if(obj)
17 ✔
915
      delete obj;
17 ✔
916
    return nullptr;
17 ✔
917
  }
918
}
39 ✔
919

920
// Preset data: {"Controls": [...], "State": [...]} where "State" is the
921
// script state set through Script.replaceState, plus the program: "Root"
922
// (source .qml file, also used for includes) and "Script" / "Ui" when they
923
// differ from that file. A plain array of controls is also accepted; its
924
// script is then the effect of the preset key.
925
void ProcessModel::loadPreset(const Process::Preset& preset)
12 ✔
926
{
927
  const rapidjson::Document doc = readJson(preset.data);
12 ✔
928
  const bool object = doc.IsObject();
12 ✔
929
  if(!object && !doc.IsArray())
12 ✔
930
    return;
×
931

932
  std::optional<QmlSource> program;
12 ✔
933
  if(object)
12 ✔
934
  {
935
    QString root;
11 ✔
936
    if(auto it = doc.FindMember("Root"); it != doc.MemberEnd() && it->value.IsString())
13 ✔
937
      root = score::locateFilePath(
2 ✔
938
          QString::fromUtf8(it->value.GetString(), it->value.GetStringLength()),
2 ✔
939
          score::IDocument::documentContext(*this));
2 ✔
940
    m_root = root;
11 ✔
941

942
    if(auto it = doc.FindMember("Script"); it != doc.MemberEnd() && it->value.IsString())
21 ✔
943
    {
944
      QmlSource p{
10 ✔
945
          QString::fromUtf8(it->value.GetString(), it->value.GetStringLength()), {}};
10 ✔
946
      if(auto ui = doc.FindMember("Ui"); ui != doc.MemberEnd() && ui->value.IsString())
11 ✔
947
        p.ui = QString::fromUtf8(ui->value.GetString(), ui->value.GetStringLength());
1 ✔
948
      program = std::move(p);
10 ✔
949
    }
10 ✔
950
    else if(!root.isEmpty())
1 ✔
951
    {
952
      program = readProgramFromFile(root);
1 ✔
953
    }
1 ✔
954
  }
11 ✔
955
  if(!program && !preset.key.effect.isEmpty())
12 ✔
956
    program = QmlSource{preset.key.effect, m_program.ui};
1 ✔
957

958
  if(program && *program != m_program)
12 ✔
959
    (void)setProgram(*program);
5 ✔
960
  else
961
    updateFileLink();
7 ✔
962

963
  if(!object)
12 ✔
964
  {
965
    Process::loadFixedControls(doc.GetArray(), *this);
1 ✔
966
    return;
1 ✔
967
  }
968

969
  if(auto it = doc.FindMember("Controls"); it != doc.MemberEnd() && it->value.IsArray())
22 ✔
970
    Process::loadFixedControls(it->value.GetArray(), *this);
11 ✔
971

972
  JSState st;
11 ✔
973
  if(auto it = doc.FindMember("State"); it != doc.MemberEnd())
11 ✔
974
    st <<= JsonValue{it->value};
11 ✔
975
  setState(st);
11 ✔
976
}
12 ✔
977

978
Process::Preset ProcessModel::savePreset() const noexcept
12 ✔
979
{
980
  auto p = Process::saveScriptProcessPreset(*this, effect());
12 ✔
981

982
  JSONReader r;
12 ✔
983
  r.stream.StartObject();
12 ✔
984
  if(!m_root.isEmpty())
12 ✔
985
    r.obj["Root"] = score::relativizeFilePath(m_root);
2 ✔
986
  if(!followsRootFile())
12 ✔
987
  {
988
    r.obj["Script"] = m_program.execution;
11 ✔
989
    if(!m_program.ui.isEmpty())
11 ✔
990
      r.obj["Ui"] = m_program.ui;
1 ✔
991
  }
11 ✔
992
  r.stream.Key("Controls");
12 ✔
993
  Process::saveFixedControls(r, *this);
12 ✔
994
  r.obj["State"] = m_state;
12 ✔
995
  r.stream.EndObject();
12 ✔
996
  p.data = r.toByteArray();
12 ✔
997
  return p;
12 ✔
998
}
12 ✔
999

1000
// Also matches presets that identify a file-based script by its text
1001
bool ProcessModel::presetMatches(const Process::Preset& preset) const noexcept
3 ✔
1002
{
1003
  return preset.key.key == concreteKey()
6 ✔
1004
         && (preset.key.effect == effect() || preset.key.effect == QString{m_qmlData});
3 ✔
1005
}
1006

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