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

ossia / score / 36173285676

25 Sep 2026 06:25PM UTC coverage: 42.611% (+0.4%) from 42.23%
36173285676

Pull #2308

github

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

529 of 703 new or added lines in 51 files covered. (75.25%)

197 existing lines in 23 files now uncovered.

127086 of 298248 relevant lines covered (42.61%)

118390.02 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