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

ossia / score / 35899133125

23 Sep 2026 05:57PM UTC coverage: 41.488% (+1.2%) from 40.337%
35899133125

push

github

jcelerier
3rdparty: follow the merged libossia

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

123236 of 297041 relevant lines covered (41.49%)

113626.05 hits per line

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

66.23
/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,488✔
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(
54✔
72
    const TimeVal& duration, const QString& data, const Id<Process::ProcessModel>& id,
73
    QObject* parent)
74
    : Process::ProcessModel{
54✔
75
        duration, id, Metadata<ObjectKey_k, ProcessModel>::get(), parent}
54✔
76
{
54✔
77
  if(data.isEmpty())
54✔
78
  {
79
    (void)setProgram({default_js_program, default_js_ui});
37✔
80
  }
37✔
81
  else
82
  {
83
    if(!data.endsWith(".qml")) {
17✔
84
      (void)setProgram({data, {}});
1✔
85
    }
1✔
86
    else {
87
      m_root = data;
16✔
88
      (void)setProgram(readProgramFromFile(data));
16✔
89
    }
90
  }
91

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

95
Process::ProcessFlags ProcessModel::flags() const noexcept
1,473✔
96
{
97
  auto flags = Metadata<Process::ProcessFlags_k, JS::ProcessModel>::get();
1,473✔
98
  if(m_ui_component)
1,473✔
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,473✔
102
}
103

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

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

117
  auto& ctx = score::IDocument::documentContext(*this);
8✔
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())
8✔
123
    map.readOnly(m_root, score::FileKind::Script);
5✔
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;
8✔
128
  bool changed = false;
8✔
129
  const auto relocate = [&](QString& script) {
24✔
130
    if(!Process::looksLikeExistingFile(script, ctx))
16✔
131
      return;
16✔
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
  };
16✔
146
  relocate(next.execution);
8✔
147
  relocate(next.ui);
8✔
148

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

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

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

173
void ProcessModel::updateFileLink() noexcept
96✔
174
{
175
  const auto file = m_root.isEmpty() ? QmlSource{} : readProgramFromFile(m_root);
96✔
176
  m_modified = m_root.isEmpty() || m_program != file;
96✔
177

178
  // An unreadable file has nothing to offer: leave the copy the document holds
179
  // alone rather than proposing to replace it with nothing.
180
  const bool outOfDate = m_modified && !file.execution.isEmpty();
96✔
181
  if(outOfDate != m_outOfDate)
96✔
182
  {
183
    m_outOfDate = outOfDate;
11✔
184
    externalSourceOutOfDateChanged(outOfDate);
11✔
185
  }
11✔
186
}
96✔
187

188
score::Command*
189
ProcessModel::refreshFromExternalSource(const score::DocumentContext& ctx) const
4✔
190
{
191
  if(!m_outOfDate)
4✔
192
    return nullptr;
3✔
193

194
  auto next = readProgramFromFile(m_root);
1✔
195
  if(next.execution.isEmpty())
1✔
196
    return nullptr;
×
197

198
  return new JS::EditScript{*this, std::move(next), ctx};
1✔
199
}
4✔
200

201
QString ProcessModel::rootPath() const noexcept
117✔
202
{
203
  if(!m_root.isEmpty())
117✔
204
  {
205
    return m_root;
49✔
206
  }
207
  else
208
  {
209
    // Not cached: tests run several applications in one process
210
    const auto& lib = score::AppContext().settings<Library::Settings::Model>();
68✔
211

212
    return lib.getDefaultLibraryPath() + QDir::separator() + "Scripts"
136✔
213
           + QDir::separator() + "include" + QDir::separator() + "Script/Script.qml";
68✔
214
  }
215
}
117✔
216

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

224
  const auto trimmed = script[0].trimmed();
6✔
225
  const QByteArray data = trimmed.toUtf8();
6✔
226

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

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

241

242
QString ProcessModel::effect() const noexcept
245✔
243
{
244
  return m_qmlData;
245✔
245
}
246

247
QQuickItem* ProcessModel::createItemForUI(const score::DocumentContext& ctx) const noexcept
32✔
248
{
249
  if(!m_ui_component)
32✔
250
    return nullptr;
3✔
251
  auto& dummyEngine =  ctx.app
29✔
252
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
29✔
253
                          .m_scriptProcessUIEngine;
29✔
254

255
  auto obj = m_ui_component->beginCreate(dummyEngine.rootContext());
29✔
256

257
  if(!obj)
29✔
258
    return nullptr;
×
259
  auto script = qobject_cast<ScriptUI*>(obj);
29✔
260
  auto self = const_cast<JS::ProcessModel*>(this);
29✔
261
  if(script) {
29✔
262
    script->setProcess(self);
29✔
263
  }
29✔
264
  m_ui_component->completeCreate();
29✔
265

266
  if(!script) {
29✔
267
    delete obj;
×
268
    return nullptr;
×
269
  }
270

271
  if(const auto& on_exec = script->executionEvent(); on_exec.isCallable())
29✔
272
  {
273
    connect(this, &JS::ProcessModel::executionToUi,
×
274
            script, [&dummyEngine, on_exec] (const QVariant& v) {
×
275
      on_exec.call({dummyEngine.toScriptValue(v)});
×
276
    });
×
277
  }
×
278

279
  connect(script, &ScriptUI::executionSend, this, [self](const QJSValue& v) {
29✔
280
    self->uiToExecution(v.toVariant());
×
281
  });
×
282

283
  struct StateUpdater
284
  {
285
    const score::DocumentContext& ctx;
286
    JS::ProcessModel& self;
287
    std::unique_ptr<MultiOngoingCommandDispatcher> disp;
288
    int count = 0;
289

290
    void beginUpdateState(const QString& name)
×
291
    {
292
      if(count > 0) {
×
293
        count++;
×
294
      }
×
295
      else {
296
        disp = std::make_unique<MultiOngoingCommandDispatcher>(ctx.commandStack);
×
297
        count = 1;
×
298
      }
299
    }
×
300

301
    void endUpdateState()
×
302
    {
303
      if(!disp)
×
304
        return;
×
305
      count--;
×
306
      if(count > 0)
×
307
        return;
×
308

309
      disp->commit<JS::UpdateStateMacro>();
×
310
      disp.reset();
×
311
      count = 0;
×
312
    }
×
313

314
    void updateState(const QString& k, const QJSValue& v)
×
315
    {
316
      beginUpdateState("Update");
×
317

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

320
      endUpdateState();
×
321
    }
×
322

323
    void cancelUpdateState()
×
324
    {
325
      if(!disp)
×
326
        return;
×
327

328
      disp->rollback();
×
329
      disp.reset();
×
330
      count = 0;
×
331
    }
×
332

333
    void clearState()
×
334
    {
335
      beginUpdateState("Clear");
×
336

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

339
      endUpdateState();
×
340
    }
×
341

342
    void replaceState(const QJSValue& v)
×
343
    {
344
      beginUpdateState("Replace");
×
345

346
      JS::JSState cur;
×
347
      auto var = v.toVariant().toMap();
×
348
      for(auto it = var.constBegin(); it != var.constEnd(); ++it) {
×
349
        if(it.value().isValid()) {
×
350
          cur.insert_or_assign(it.key(), ossia::qt::qt_to_ossia{}(it.value()));
×
351
        }
×
352
      }
×
353
      disp->submit<JS::ReplaceState>(self, std::move(cur));
×
354

355
      endUpdateState();
×
356
    }
×
357
  };
358
  auto updater = std::make_shared<StateUpdater>(StateUpdater{ctx, *self});
29✔
359

360
  connect(script, &ScriptUI::beginUpdateState,
58✔
361
          this, [updater] (const QString& name) {
29✔
362
    updater->beginUpdateState(name);
×
363
  });
×
364
  connect(script, &ScriptUI::updateState,
58✔
365
          this, [updater] (const QString& name, const QJSValue& v) {
29✔
366
    updater->updateState(name, v);
×
367
  });
×
368
  connect(script, &ScriptUI::endUpdateState,
58✔
369
          this, [updater] () {
29✔
370
    updater->endUpdateState();
×
371
  });
×
372
  connect(script, &ScriptUI::cancelUpdateState,
58✔
373
          this, [updater] () {
29✔
374
    updater->cancelUpdateState();
×
375
  });
×
376
  connect(script, &ScriptUI::clearState,
58✔
377
          this, [updater] () {
29✔
378
    updater->clearState();
×
379
  });
×
380
  connect(script, &ScriptUI::replaceState,
58✔
381
          this, [updater] (const QJSValue& v) {
29✔
382
    updater->replaceState(v);
×
383
  });
×
384

385
  if(const auto& on_stateUpdated = script->stateUpdated(); on_stateUpdated.isCallable())
29✔
386
  {
387
    connect(
×
388
        this, &JS::ProcessModel::stateElementChanged, script,
×
389
        [on_stateUpdated, &dummyEngine](const QString& k, const ossia::value& v) {
×
390
      if(v.valid())
×
391
      {
392
        if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
393
          on_stateUpdated.call({k, dummyEngine.toScriptValue(res)});
×
394
        else
395
          on_stateUpdated.call({k, QJSValue{}});
×
396
      }
×
397
      else
398
        on_stateUpdated.call({k, QJSValue{}});
×
399
    }, Qt::QueuedConnection);
×
400
  }
×
401

402
  if(const auto& on_load = script->loadState(); on_load.isCallable())
29✔
403
  {
404
    QVariantMap vm;
×
405
    for(auto& [k, v]: this->m_state) {
×
406
      if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
407
        vm[k] = std::move(res);
×
408
    }
409
    on_load.call({dummyEngine.toScriptValue(vm)});
×
410
  }
×
411

412
  return script;
29✔
413
}
32✔
414

415
QWidget* ProcessModel::createWindowForUI(const score::DocumentContext& ctx,
23✔
416
                                         QWidget* parent) const noexcept
417
{
418
  m_ui_object = createItemForUI(ctx);
23✔
419
  if(!m_ui_object)
23✔
420
    return nullptr;
×
421

422
  // A close is deferred: by the time this runs the process may show a new
423
  // UI, which must be left alone. The container pointer is only compared,
424
  // the widget may be half-destroyed.
425
  const auto cleanup_ui = [this](QQuickWindow* win, QWidget* container) {
45✔
426
    if(m_ui_object && m_ui_object->window() == win)
22✔
427
    {
428
      delete m_ui_object;
×
429
      m_ui_object = nullptr;
×
430
    }
×
431

432
    if(externalUI == container)
22✔
433
    {
434
      const_cast<QWidget*&>(externalUI) = nullptr;
×
435
      externalUIVisible(false);
×
436
    }
×
437
  };
22✔
438

439
  // Let the UI take the whole window: needed when the window is docked in
440
  // the main window and follows the size of its pane.
441
  const auto fitToWindow = [](QQuickWindow* win, QQuickItem* item) {
50✔
442
    if(win && item)
27✔
443
      item->setSize(QSizeF(win->width(), win->height()));
27✔
444
  };
27✔
445

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

453
    delete m_ui_object;
3✔
454
    m_ui_object = nullptr;
3✔
455
    if(!m_ui_component)
3✔
456
      return false;
×
457

458
    m_ui_object = createItemForUI(ctx);
3✔
459
    if(!m_ui_object)
3✔
460
      return false;
×
461
    m_ui_object->setParentItem(win->contentItem());
3✔
462
    m_ui_object->setParent(win->contentItem());
3✔
463
    fitToWindow(win, m_ui_object);
3✔
464
    return true;
3✔
465
  };
3✔
466

467
  // The requested size: the ScriptUI root asks for one through its implicit
468
  // width / height (e.g. `implicitWidth: 1280`), defaulting to 640x640.
469
  const QSize requested = [this] {
46✔
470
    int w = 640, h = 640;
23✔
471
    if(m_ui_object->implicitWidth() >= 100.)
23✔
472
      w = static_cast<int>(m_ui_object->implicitWidth());
20✔
473
    if(m_ui_object->implicitHeight() >= 100.)
23✔
474
      h = static_cast<int>(m_ui_object->implicitHeight());
20✔
475
    return QSize{w, h};
23✔
476
  }();
477

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

497
  m_ui_object->setParentItem(win->contentItem());
23✔
498
  connect(win, &QQuickWindow::widthChanged, this, [this, win, fitToWindow] {
35✔
499
    fitToWindow(win, m_ui_object);
12✔
500
  });
12✔
501
  connect(win, &QQuickWindow::heightChanged, this, [this, win, fitToWindow] {
35✔
502
    fitToWindow(win, m_ui_object);
12✔
503
  });
12✔
504

505
  auto widg = QWidget::createWindowContainer(win, parent);
23✔
506
  if(!widg) {
23✔
507
    delete m_ui_object;
×
508
    m_ui_object = nullptr;
×
509
    delete win;
×
510
    return nullptr;
×
511
  }
512
  widg->setAttribute(Qt::WA_DeleteOnClose);
23✔
513
  // The container widget does not follow the QQuickWindow's size
514
  widg->resize(requested);
23✔
515

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

539
  // No externalUIVisible(true) here: setupExternalUI emits it for every
540
  // process type once the UI is placed, and only when one is actually created
541
  // -- asking again for an open UI raises it instead. Emitting a second time
542
  // from this path would make a re-request report {true, true, false}.
543
  return widg;
23✔
544
}
23✔
545

546
void ProcessModel::setExecutionScript(const QString& f)
96✔
547
{
548
  if(f == m_program.execution)
96✔
549
    return;
13✔
550
  m_program.execution = std::move(f);
83✔
551

552
  executionScriptChanged(m_program.execution);
83✔
553
}
96✔
554

555
void ProcessModel::setUiScript(const QString& f)
96✔
556
{
557
  if(f == m_program.ui)
96✔
558
    return;
66✔
559
  m_program.ui = std::move(f);
30✔
560

561
  uiScriptChanged(m_program.ui);
30✔
562
}
96✔
563

564
void ProcessModel::setState(const JSState &s)
10✔
565
{
566
  if(s == m_state)
10✔
567
    return;
10✔
568

569
  {
570
    const auto prev = std::move(m_state);
×
571
    for(auto& [prev_k, prev_v] : prev) {
×
572
      stateElementChanged(prev_k, prev_v);
×
573
    }
574
  }
×
575

576
  m_state = std::move(s);
×
577
  for(auto& [k, v] : m_state) {
×
578
    stateElementChanged(k, v);
×
579
  }
580

581
  stateChanged();
×
582
}
10✔
583

584
void ProcessModel::updateState(const QString &k, const ossia::value& res)
×
585
{
586
  if(auto it = m_state.find(k); it != m_state.end())
×
587
  {
588
    if(res.valid())
×
589
    {
590
      if(res != it->second)
×
591
      {
592
        // Updating a new element
593
        m_state[k] = res;
×
594
        stateElementChanged(k, res);
×
595
        stateChanged();
×
596
      }
×
597
    }
×
598
    else
599
    {
600
      // Removing an element
601
      m_state.erase(k);
×
602
      stateElementChanged(k, res);
×
603
      stateChanged();
×
604
    }
605
  }
×
606
  else
607
  {
608
    if(res.valid())
×
609
    {
610
      // Adding a new element
611
      m_state[k] = res;
×
612
      stateElementChanged(k, res);
×
613
      stateChanged();
×
614
    }
×
615
    else
616
    {
617
      // Already not there, nothing to do
618
    }
619
  }
620
}
×
621

622
[[nodiscard]] Process::ScriptChangeResult ProcessModel::setProgram(const JS::QmlSource& script)
96✔
623
{
624
  setExecutionScript(script.execution);
96✔
625
  setUiScript(script.ui);
96✔
626

627
  Process::ScriptChangeResult res;
96✔
628
  const auto trimmed = script.execution.trimmed();
96✔
629
  const QByteArray data = trimmed.toUtf8();
96✔
630

631
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
96✔
632

633
  // setQmlData builds the UI component from m_program.ui: it must be the new
634
  // one, otherwise every compile shows the UI of the previous compile.
635
  const auto previous = m_program;
96✔
636
  m_program = script;
96✔
637

638
  if(QFileInfo::exists(path))
96✔
639
    res = setQmlData(path.toUtf8(), true);
×
640
  else
641
    res = setQmlData(data, false);
96✔
642

643
  if(!res.valid)
96✔
644
    m_program = previous;
3✔
645
  updateFileLink();
96✔
646
  return res;
96✔
647
}
96✔
648

649
Process::ScriptChangeResult ProcessModel::setQmlData(const QByteArray& data, bool isFile)
96✔
650
{
651
  Process::ScriptChangeResult res;
96✔
652
  if(!isFile && !data.contains("import "))
96✔
653
    return res;
×
654

655
  auto script = m_cache.getExecution(*this, data, isFile);
96✔
656
  if(!script)
96✔
657
    return res;
3✔
658

659
  m_isFile = isFile;
93✔
660
  m_qmlData = data;
93✔
661

662
  res.inlets = score::clearAndDeleteLater(m_inlets);
93✔
663
  res.outlets = score::clearAndDeleteLater(m_outlets);
93✔
664
  const bool had_ui = m_ui_component;
93✔
665
  m_ui_component = nullptr;
93✔
666
  delete m_ui_object;
93✔
667
  m_ui_object = nullptr;
93✔
668

669
  SCORE_ASSERT(m_inlets.size() == 0);
93✔
670
  SCORE_ASSERT(m_outlets.size() == 0);
93✔
671

672
  // Check inlets / outlets
673
  {
674
    auto cld_inlet = script->findChildren<Inlet*>();
93✔
675
    int i = 0;
93✔
676
    for(auto n : cld_inlet)
228✔
677
    {
678
      auto port = n->make(Id<Process::Port>(i++), this);
135✔
679
      if(const auto& name = n->objectName(); !name.isEmpty())
233✔
680
        port->setName(name);
98✔
681
      if(auto addr = State::parseAddressAccessor(n->address()))
135✔
682
        port->setAddress(std::move(*addr));
×
683
      m_inlets.push_back(port);
135✔
684
    }
685
  }
93✔
686

687
  {
688
    auto cld_outlet = script->findChildren<Outlet*>();
93✔
689
    int i = 0;
93✔
690
    for(auto n : cld_outlet)
182✔
691
    {
692
      auto port = n->make(Id<Process::Port>(i++), this);
89✔
693
      if(const auto& name = n->objectName(); !name.isEmpty())
145✔
694
        port->setName(name);
56✔
695
      if(auto addr = State::parseAddressAccessor(n->address()))
89✔
696
        port->setAddress(std::move(*addr));
×
697
      m_outlets.push_back(port);
89✔
698
    }
699
  }
93✔
700

701
  // Create ui if any
702
  if(!this->m_program.ui.isEmpty()) {
93✔
703
    m_ui_component = m_cache.getUi(*this, this->m_program.ui.toUtf8(), isFile);
37✔
704
  }
37✔
705

706
  if(m_isFile)
93✔
707
  {
708
    const auto name = QFileInfo{data}.baseName();
×
709
    metadata().setName(name);
×
710
    metadata().setLabel(name);
×
711
  }
×
712
  else if(metadata().getName().isEmpty())
93✔
713
  {
714
    metadata().setName(QStringLiteral("Script"));
52✔
715
  }
52✔
716

717
  executionScriptOk();
93✔
718
  res.valid = true;
93✔
719

720
  if(bool(m_ui_component) != had_ui)
93✔
721
    flagsChanged();
9✔
722

723
  if(m_ui_component)
93✔
724
  {
725
    uiScriptOk();
11✔
726
  }
11✔
727
  else if(externalUI)
82✔
728
  {
729
    externalUI->close();
×
730
    externalUI->deleteLater();
×
731
    externalUI = nullptr;
×
732
    externalUIVisible(false);
×
733
  }
×
734

735
  // inlets / outletsChanged : in ScriptEditCommand
736
  return res;
93✔
737
}
96✔
738

739
Script* ProcessModel::currentExecutionObject() const noexcept
4✔
740
{
741
  if(auto cache = m_cache.tryGet(m_qmlData, m_isFile))
4✔
742
    return cache->object.get();
2✔
743
  return nullptr;
2✔
744
}
4✔
745

746
bool ProcessModel::isGpu() const noexcept
4✔
747
{
748
#if defined(SCORE_HAS_GPU_JS)
749
  if(auto script = currentExecutionObject())
750
  {
751
    return
752
        script->findChild<JS::TextureInlet*>() != nullptr
753
           || script->findChild<JS::TextureOutlet*>() != nullptr
754
           // || script->findChild<JS::BufferInlet*>() != nullptr
755
           // || script->findChild<JS::BufferOutlet*>() != nullptr
756
        ;
757
  }
758
#endif
759
  return false;
4✔
760
}
761

762
ComponentCache::ComponentCache() { }
64✔
763
ComponentCache::~ComponentCache() { }
64✔
764

765
QByteArray ComponentCache::key(const QByteArray& str, bool isFile) noexcept
230✔
766
{
767
  if(!isFile)
230✔
768
    return str;
230✔
769

770
  // What is cached is a compiled script, so what identifies it is the text it
771
  // was compiled from -- not the name of the file that happened to hold it,
772
  // which says nothing about whether that file still says the same thing.
773
  QFile f{QString::fromUtf8(str)};
×
774
  if(!f.open(QIODevice::ReadOnly))
×
775
    return {};
×
776
  return f.readAll();
×
777
}
230✔
778

779
const ComponentCache::Cache* ComponentCache::tryGet(const QByteArray& str, bool isFile) const noexcept
143✔
780
{
781
  const QByteArray content = key(str, isFile);
143✔
782
  if(content.isEmpty())
143✔
783
    return nullptr;
2✔
784

785
  if(auto it = ossia::find_if(m_map, [&](const auto& k) { return k.key == content; });
253✔
786
     it != m_map.end())
141✔
787
  {
788
    return &*it;
25✔
789
  }
790
  return nullptr;
116✔
791
}
143✔
792

793
Script* ComponentCache::getExecution(
102✔
794
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
795
{
796
  if(auto cache = tryGet(str, isFile))
102✔
797
    return cache->object.get();
21✔
798

799
  auto& dummyEngine = score::GUIAppContext()
162✔
800
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
81✔
801
                          .m_scriptProcessUIEngine;
81✔
802
  std::unique_ptr<QQmlComponent> comp;
81✔
803
  if(!isFile)
81✔
804
  {
805
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
81✔
806
    loadJSObjectFromString(process.rootPath(), str, *comp, false);
81✔
807
  }
81✔
808
  else
809
  {
810
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
811
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
812
  }
813

814
  const auto& errs = comp->errors();
81✔
815
  if(!errs.empty())
81✔
816
  {
817
    const auto& err = errs.first();
3✔
818
    qDebug() << err.line() << err.toString();
3✔
819
    auto str = err.toString();
3✔
820
    str.remove("<Unknown File>:");
3✔
821
    process.errorMessage(/* err.line(), */str);
3✔
822
    return nullptr;
3✔
823
  }
3✔
824

825
  auto obj = comp->create();
78✔
826
  auto script = qobject_cast<JS::Script*>(obj);
78✔
827
  if(script)
78✔
828
  {
829
    if(m_map.size() > 5)
78✔
830
      m_map.erase(m_map.begin());
×
831

832
    m_map.emplace_back(
156✔
833
        Cache{key(str, isFile), std::move(comp), std::unique_ptr<JS::Script>(script)});
78✔
834
    return script;
78✔
835
  }
836
  else
837
  {
838
    process.errorMessage(/* 0, */"The component must be of type Script");
×
839
    if(obj)
×
840
    {
841
      delete obj;
×
842
    }
×
843
    return nullptr;
×
844
  }
845
}
102✔
846

847
QQmlComponent* ComponentCache::getUi(
37✔
848
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
849
{
850
  if(auto cache = tryGet(str, isFile))
37✔
851
    return cache->component.get();
2✔
852

853
  auto& dummyEngine = score::GUIAppContext()
70✔
854
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
35✔
855
                          .m_scriptProcessUIEngine;
35✔
856

857
  std::unique_ptr<QQmlComponent> comp;
35✔
858
  if(!isFile)
35✔
859
  {
860
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
35✔
861
    loadJSObjectFromString(process.rootPath(), str, *comp, true);
35✔
862
  }
35✔
863
  else
864
  {
865
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
866
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
867
  }
868

869
  const auto& errs = comp->errors();
35✔
870
  if(!errs.empty())
35✔
871
  {
872
    const auto& err = errs.first();
11✔
873
    qDebug() << err.line() << err.toString();
11✔
874
    auto str = err.toString();
11✔
875
    str.remove("<Unknown File>:");
11✔
876
    process.errorMessage(/* err.line(), */str);
11✔
877
    return nullptr;
11✔
878
  }
11✔
879

880
  auto obj = comp->beginCreate(dummyEngine.rootContext());
24✔
881
  if(!obj) {
24✔
882
    process.errorMessage(/* 0, */"Cannot create UI object");
×
883
    return nullptr;
×
884
  }
885
  auto script = qobject_cast<ScriptUI*>(obj);
24✔
886
  if(script) {
24✔
887
    script->setProcess((Process::ProcessModel*)&process);
9✔
888
  }
9✔
889
  comp->completeCreate();
24✔
890
  if(script)
24✔
891
  {
892
    if(m_map.size() > 5)
9✔
893
      m_map.erase(m_map.begin());
×
894

895
    m_map.emplace_back(
18✔
896
        Cache{key(str, isFile), std::move(comp), {}});
9✔
897
    delete script;
9✔
898
    return m_map.back().component.get();
9✔
899
  }
900
  else
901
  {
902
    process.errorMessage(/* 0, */"The component must be of type Script");
15✔
903
    if(obj)
15✔
904
      delete obj;
15✔
905
    return nullptr;
15✔
906
  }
907
}
37✔
908

909
void ProcessModel::loadPreset(const Process::Preset& preset)
×
910
{
911
  Process::loadScriptProcessPreset<ProcessModel::p_program>(*this, preset);
×
912
}
×
913

914
Process::Preset ProcessModel::savePreset() const noexcept
×
915
{
916
  // FIXME this should save p_program
917
  return Process::saveScriptProcessPreset(*this, this->m_qmlData);
×
918
}
919

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