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

ossia / score / 34154826796

07 Sep 2026 07:14PM UTC coverage: 28.003% (+1.6%) from 26.366%
34154826796

push

github

jcelerier
dataflow: refilling a combo box's items is not a domain change

Dragging a control that causes a combo box to be refilled moved the value by
one step and then stopped, left a widget behind, and could leave a right-click
editor on the scene that nothing would close again.

setAlternatives restated the domain, and DefaultEffectItem answers
domainChanged by deleting every control of the process and rebuilding them from
the event loop. So the sequence was: drag the control -> the running object
refills the combo box -> setDomain -> domainChanged -> the control being
dragged is destroyed. The pointer then had nothing to drag, and anything the
destroyed control had put on the scene outlived the item that knew how to take
it down.

The item list is what changed, and alternativesChanged already says so -- the
widgets rebuild from it in place. The domain still follows the items for
whoever reads it later; it just no longer claims the control's shape changed.

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

2 of 2 new or added lines in 2 files covered. (100.0%)

1657 existing lines in 32 files now uncovered.

66180 of 236333 relevant lines covered (28.0%)

63513.54 hits per line

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

56.89
/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)
1,946✔
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(
32✔
72
    const TimeVal& duration, const QString& data, const Id<Process::ProcessModel>& id,
73
    QObject* parent)
74
    : Process::ProcessModel{
32✔
75
        duration, id, Metadata<ObjectKey_k, ProcessModel>::get(), parent}
32✔
76
{
32✔
77
  if(data.isEmpty())
32✔
78
  {
79
    (void)setProgram({default_js_program, default_js_ui});
32✔
80
  }
32✔
81
  else
82
  {
83
    if(!data.endsWith(".qml")) {
×
84
      (void)setProgram({data, {}});
×
85
    }
×
86
    else {
87
      auto path = data;
×
88
      QFile f{path};
×
89
      m_root = path;
×
90
      QString exec_data;
×
91
      if(f.open(QIODevice::ReadOnly))
×
92
        exec_data = f.readAll();
×
93

94
      path.resize(data.size() - 3);
×
95
      path.append("ui.qml");
×
96
      QString ui_data;
×
97
      if(QFile ui_f{path}; ui_f.open(QIODevice::ReadOnly)) {
×
98
        ui_data = ui_f.readAll();
×
99
      }
×
100
      (void)setProgram({exec_data, ui_data});
×
101
    }
×
102
  }
103

104
  metadata().setInstanceName(*this);
32✔
105
}
×
106

107
Process::ProcessFlags ProcessModel::flags() const noexcept
913✔
108
{
109
  auto flags = Metadata<Process::ProcessFlags_k, JS::ProcessModel>::get();
913✔
110
  if(m_ui_component)
913✔
111
    // The UI is a Qt Quick scene in a window container: it can be docked.
112
    flags |= Process::ExternalUIAvailable | Process::ExternalUIEmbeddable;
145✔
113
  return flags;
913✔
114
}
115

116
ProcessModel::~ProcessModel()
74✔
117
{
37✔
118
  if(this->externalUI)
37✔
119
  {
120
    this->externalUI->close();
1✔
121
    this->externalUI = nullptr;
1✔
122
  }
1✔
123
}
74✔
124

125
void ProcessModel::mapExternalFiles(Process::ExternalFileMap& map)
3✔
126
{
127
  Process::ProcessModel::mapExternalFiles(map);
3✔
128

129
  auto& ctx = score::IDocument::documentContext(*this);
3✔
130

131
  // The QML import root. Relocating it means collecting a whole include tree,
132
  // which score does not attempt: report it so the user knows the other
133
  // machine needs it.
134
  if(!m_root.isEmpty())
3✔
135
    map.readOnly(m_root, score::FileKind::Script);
×
136

137
  // A script is stored either inline or as a path to a .qml file; only the
138
  // latter is an external dependency.
139
  QmlSource next = m_program;
3✔
140
  bool changed = false;
3✔
141
  const auto relocate = [&](QString& script) {
9✔
142
    if(!Process::looksLikeExistingFile(script, ctx))
6✔
143
      return;
6✔
144

145
    const QString relocated = map.map(
×
146
        {.path = script,
×
147
         .kind = score::FileKind::Script,
148
         .usage = Process::FileUsage::Input,
149
         .directory = false,
150
         .rewritable = true,
151
         .owner = map.owner});
×
152
    if(relocated.isEmpty())
×
153
      return;
×
154

155
    script = relocated;
×
156
    changed = true;
×
157
  };
6✔
158
  relocate(next.execution);
3✔
159
  relocate(next.ui);
3✔
160

161
  if(changed)
3✔
162
    map.addCommand(new JS::EditScript{*this, next, ctx});
×
163
}
3✔
164

165
QString ProcessModel::rootPath() const noexcept
52✔
166
{
167
  if(!m_root.isEmpty())
52✔
168
  {
169
    return m_root;
×
170
  }
171
  else
172
  {
173
    // Not cached: tests run several applications in one process
174
    const auto& lib = score::AppContext().settings<Library::Settings::Model>();
52✔
175

176
    return lib.getDefaultLibraryPath() + QDir::separator() + "Scripts"
104✔
177
           + QDir::separator() + "include" + QDir::separator() + "Script/Script.qml";
52✔
178
  }
179
}
52✔
180

181
bool ProcessModel::validate(const std::vector<QString>& script) const noexcept
6✔
182
{
183
  if(script.empty())
6✔
184
    return false;
×
185
  if(script[0].isEmpty())
6✔
186
    return false;
×
187

188
  const auto trimmed = script[0].trimmed();
6✔
189
  const QByteArray data = trimmed.toUtf8();
6✔
190

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

193
  if(QFileInfo::exists(path))
6✔
194
  {
195
    return (bool)m_cache.getExecution(*this, path.toUtf8(), true);
×
196
  }
197
  else
198
  {
199
    if(!data.startsWith("import"))
6✔
200
      return false;
×
201
    return (bool)m_cache.getExecution(*this, data, false);
6✔
202
  }
203
}
6✔
204

205

206
QString ProcessModel::effect() const noexcept
76✔
207
{
208
  return m_qmlData;
76✔
209
}
210

211
QQuickItem* ProcessModel::createItemForUI(const score::DocumentContext& ctx) const noexcept
23✔
212
{
213
  if(!m_ui_component)
23✔
214
    return nullptr;
×
215
  auto& dummyEngine =  ctx.app
23✔
216
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
23✔
217
                          .m_scriptProcessUIEngine;
23✔
218

219
  auto obj = m_ui_component->beginCreate(dummyEngine.rootContext());
23✔
220

221
  if(!obj)
23✔
222
    return nullptr;
×
223
  auto script = qobject_cast<ScriptUI*>(obj);
23✔
224
  auto self = const_cast<JS::ProcessModel*>(this);
23✔
225
  if(script) {
23✔
226
    script->setProcess(self);
23✔
227
  }
23✔
228
  m_ui_component->completeCreate();
23✔
229

230
  if(!script) {
23✔
231
    delete obj;
×
232
    return nullptr;
×
233
  }
234

235
  if(const auto& on_exec = script->executionEvent(); on_exec.isCallable())
23✔
236
  {
237
    connect(this, &JS::ProcessModel::executionToUi,
×
238
            script, [&dummyEngine, on_exec] (const QVariant& v) {
×
239
      on_exec.call({dummyEngine.toScriptValue(v)});
×
240
    });
×
241
  }
×
242

243
  connect(script, &ScriptUI::executionSend, this, [self](const QJSValue& v) {
23✔
244
    self->uiToExecution(v.toVariant());
×
245
  });
×
246

247
  struct StateUpdater
248
  {
249
    const score::DocumentContext& ctx;
250
    JS::ProcessModel& self;
251
    std::unique_ptr<MultiOngoingCommandDispatcher> disp;
252
    int count = 0;
253

254
    void beginUpdateState(const QString& name)
×
255
    {
256
      if(count > 0) {
×
257
        count++;
×
258
      }
×
259
      else {
260
        disp = std::make_unique<MultiOngoingCommandDispatcher>(ctx.commandStack);
×
261
        count = 1;
×
262
      }
263
    }
×
264

265
    void endUpdateState()
×
266
    {
267
      if(!disp)
×
268
        return;
×
269
      count--;
×
270
      if(count > 0)
×
271
        return;
×
272

273
      disp->commit<JS::UpdateStateMacro>();
×
274
      disp.reset();
×
275
      count = 0;
×
276
    }
×
277

278
    void updateState(const QString& k, const QJSValue& v)
×
279
    {
280
      beginUpdateState("Update");
×
281

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

284
      endUpdateState();
×
285
    }
×
286

287
    void cancelUpdateState()
×
288
    {
289
      if(!disp)
×
290
        return;
×
291

292
      disp->rollback();
×
293
      disp.reset();
×
294
      count = 0;
×
295
    }
×
296

297
    void clearState()
×
298
    {
299
      beginUpdateState("Clear");
×
300

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

303
      endUpdateState();
×
304
    }
×
305

306
    void replaceState(const QJSValue& v)
×
307
    {
308
      beginUpdateState("Replace");
×
309

310
      JS::JSState cur;
×
311
      auto var = v.toVariant().toMap();
×
312
      for(auto it = var.constBegin(); it != var.constEnd(); ++it) {
×
313
        if(it.value().isValid()) {
×
314
          cur.insert_or_assign(it.key(), ossia::qt::qt_to_ossia{}(it.value()));
×
315
        }
×
316
      }
×
317
      disp->submit<JS::ReplaceState>(self, std::move(cur));
×
318

319
      endUpdateState();
×
320
    }
×
321
  };
322
  auto updater = std::make_shared<StateUpdater>(StateUpdater{ctx, *self});
23✔
323

324
  connect(script, &ScriptUI::beginUpdateState,
46✔
325
          this, [updater] (const QString& name) {
23✔
326
    updater->beginUpdateState(name);
×
327
  });
×
328
  connect(script, &ScriptUI::updateState,
46✔
329
          this, [updater] (const QString& name, const QJSValue& v) {
23✔
330
    updater->updateState(name, v);
×
331
  });
×
332
  connect(script, &ScriptUI::endUpdateState,
46✔
333
          this, [updater] () {
23✔
334
    updater->endUpdateState();
×
335
  });
×
336
  connect(script, &ScriptUI::cancelUpdateState,
46✔
337
          this, [updater] () {
23✔
338
    updater->cancelUpdateState();
×
339
  });
×
340
  connect(script, &ScriptUI::clearState,
46✔
341
          this, [updater] () {
23✔
342
    updater->clearState();
×
343
  });
×
344
  connect(script, &ScriptUI::replaceState,
46✔
345
          this, [updater] (const QJSValue& v) {
23✔
346
    updater->replaceState(v);
×
347
  });
×
348

349
  if(const auto& on_stateUpdated = script->stateUpdated(); on_stateUpdated.isCallable())
23✔
350
  {
351
    connect(
×
352
        this, &JS::ProcessModel::stateElementChanged, script,
×
353
        [on_stateUpdated, &dummyEngine](const QString& k, const ossia::value& v) {
×
354
      if(v.valid())
×
355
      {
356
        if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
357
          on_stateUpdated.call({k, dummyEngine.toScriptValue(res)});
×
358
        else
359
          on_stateUpdated.call({k, QJSValue{}});
×
360
      }
×
361
      else
362
        on_stateUpdated.call({k, QJSValue{}});
×
363
    }, Qt::QueuedConnection);
×
364
  }
×
365

366
  if(const auto& on_load = script->loadState(); on_load.isCallable())
23✔
367
  {
368
    QVariantMap vm;
×
369
    for(auto& [k, v]: this->m_state) {
×
370
      if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
371
        vm[k] = std::move(res);
×
372
    }
373
    on_load.call({dummyEngine.toScriptValue(vm)});
×
374
  }
×
375

376
  return script;
23✔
377
}
23✔
378

379
QWidget* ProcessModel::createWindowForUI(const score::DocumentContext& ctx,
20✔
380
                                         QWidget* parent) const noexcept
381
{
382
  m_ui_object = createItemForUI(ctx);
20✔
383
  if(!m_ui_object)
20✔
384
    return nullptr;
×
385

386
  // A close is deferred: by the time this runs the process may show a new
387
  // UI, which must be left alone. The container pointer is only compared,
388
  // the widget may be half-destroyed.
389
  const auto cleanup_ui = [this](QQuickWindow* win, QWidget* container) {
39✔
390
    if(m_ui_object && m_ui_object->window() == win)
19✔
391
    {
392
      delete m_ui_object;
×
393
      m_ui_object = nullptr;
×
394
    }
×
395

396
    if(externalUI == container)
19✔
397
    {
398
      const_cast<QWidget*&>(externalUI) = nullptr;
×
399
      externalUIVisible(false);
×
400
    }
×
401
  };
19✔
402

403
  // Let the UI take the whole window: needed when the window is docked in
404
  // the main window and follows the size of its pane.
405
  const auto fitToWindow = [](QQuickWindow* win, QQuickItem* item) {
47✔
406
    if(win && item)
27✔
407
      item->setSize(QSizeF(win->width(), win->height()));
27✔
408
  };
27✔
409

410
  // On a new compile the UI object is recreated; put it back in the window
411
  const auto reloadUI = [this, &ctx, fitToWindow](QQuickWindow* win) -> bool {
23✔
412
    // A window closed and waiting for its deletion while a new one holds
413
    // the UI: nothing to do in it
414
    if(m_ui_object && m_ui_object->window() != win)
3✔
415
      return true;
×
416

417
    delete m_ui_object;
3✔
418
    m_ui_object = nullptr;
3✔
419
    if(!m_ui_component)
3✔
420
      return false;
×
421

422
    m_ui_object = createItemForUI(ctx);
3✔
423
    if(!m_ui_object)
3✔
424
      return false;
×
425
    m_ui_object->setParentItem(win->contentItem());
3✔
426
    m_ui_object->setParent(win->contentItem());
3✔
427
    fitToWindow(win, m_ui_object);
3✔
428
    return true;
3✔
429
  };
3✔
430

431
  // The requested size: the ScriptUI root asks for one through its implicit
432
  // width / height (e.g. `implicitWidth: 1280`), defaulting to 640x640.
433
  const QSize requested = [this] {
40✔
434
    int w = 640, h = 640;
20✔
435
    if(m_ui_object->implicitWidth() >= 100.)
20✔
436
      w = static_cast<int>(m_ui_object->implicitWidth());
20✔
437
    if(m_ui_object->implicitHeight() >= 100.)
20✔
438
      h = static_cast<int>(m_ui_object->implicitHeight());
20✔
439
    return QSize{w, h};
20✔
440
  }();
441

442
  auto win = new QQuickWindow{};
20✔
443
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
444
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
445
  // leaves the window with no title bar, close or minimise button.
446
  win->setFlags(
20✔
447
      win->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
20✔
448
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
20✔
449
      | Qt::WindowMaximizeButtonHint);
20✔
450
#if defined(__EMSCRIPTEN__)
451
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
452
  // turns into showFullScreen() for every top level: the requested size is
453
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
454
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
455
  win->setFlags(win->flags() | Qt::Dialog);
456
#endif
457
  win->setWidth(640);
20✔
458
  win->setHeight(640);
20✔
459
  win->setColor(qApp->palette().color(QPalette::Window));
20✔
460

461
  m_ui_object->setParentItem(win->contentItem());
20✔
462
  connect(win, &QQuickWindow::widthChanged, this, [this, win, fitToWindow] {
32✔
463
    fitToWindow(win, m_ui_object);
12✔
464
  });
12✔
465
  connect(win, &QQuickWindow::heightChanged, this, [this, win, fitToWindow] {
32✔
466
    fitToWindow(win, m_ui_object);
12✔
467
  });
12✔
468

469
  auto widg = QWidget::createWindowContainer(win, parent);
20✔
470
  if(!widg) {
20✔
471
    delete m_ui_object;
×
472
    m_ui_object = nullptr;
×
473
    delete win;
×
474
    return nullptr;
×
475
  }
476
  widg->setAttribute(Qt::WA_DeleteOnClose);
20✔
477
  // The container widget does not follow the QQuickWindow's size
478
  widg->resize(requested);
20✔
479

480
#if QT_VERSION >= QT_VERSION_CHECK(6,8,2)
481
  // Bug in older Qt 6 versions:
482
  // QtCore/qmetatype.h:842:23: error: invalid application of 'sizeof' to an incomplete type 'QQuickCloseEvent'
483
  // static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");
484
  connect(win, &QQuickWindow::closing, this, [cleanup_ui, win, widg] {
485
    cleanup_ui(win, widg);
486
  });
487
#endif
488
  connect(win, &QQuickWindow::destroyed, this, [cleanup_ui, win, widg] {
39✔
489
    cleanup_ui(win, widg);
19✔
490
  });
19✔
491
  connect(
20✔
492
      this, &JS::ProcessModel::uiScriptOk, win,
20✔
493
      [reloadUI, win, container = QPointer<QWidget>{widg}] {
23✔
494
    if(!reloadUI(win))
3✔
495
    {
496
      // The container is what the main window holds: closing it takes the
497
      // window with it and runs the cleanup from destroyed()
498
      if(container)
×
499
        container->close();
×
500
    }
×
501
  });
3✔
502
  return widg;
20✔
503
}
20✔
504

505
void ProcessModel::setExecutionScript(const QString& f)
50✔
506
{
507
  if(f == m_program.execution)
50✔
508
    return;
9✔
509
  m_program.execution = std::move(f);
41✔
510

511
  executionScriptChanged(m_program.execution);
41✔
512
}
50✔
513

514
void ProcessModel::setUiScript(const QString& f)
50✔
515
{
516
  if(f == m_program.ui)
50✔
517
    return;
39✔
518
  m_program.ui = std::move(f);
11✔
519

520
  uiScriptChanged(m_program.ui);
11✔
521
}
50✔
522

523
void ProcessModel::setState(const JSState &s)
5✔
524
{
525
  if(s == m_state)
5✔
526
    return;
5✔
527

528
  {
529
    const auto prev = std::move(m_state);
×
530
    for(auto& [prev_k, prev_v] : prev) {
×
531
      stateElementChanged(prev_k, prev_v);
×
532
    }
533
  }
×
534

535
  m_state = std::move(s);
×
536
  for(auto& [k, v] : m_state) {
×
537
    stateElementChanged(k, v);
×
538
  }
539

540
  stateChanged();
×
541
}
5✔
542

543
void ProcessModel::updateState(const QString &k, const ossia::value& res)
×
544
{
545
  if(auto it = m_state.find(k); it != m_state.end())
×
546
  {
547
    if(res.valid())
×
548
    {
549
      if(res != it->second)
×
550
      {
551
        // Updating a new element
552
        m_state[k] = res;
×
553
        stateElementChanged(k, res);
×
554
        stateChanged();
×
555
      }
×
556
    }
×
557
    else
558
    {
559
      // Removing an element
560
      m_state.erase(k);
×
561
      stateElementChanged(k, res);
×
562
      stateChanged();
×
563
    }
564
  }
×
565
  else
566
  {
567
    if(res.valid())
×
568
    {
569
      // Adding a new element
570
      m_state[k] = res;
×
571
      stateElementChanged(k, res);
×
572
      stateChanged();
×
573
    }
×
574
    else
575
    {
576
      // Already not there, nothing to do
577
    }
578
  }
579
}
×
580

581
[[nodiscard]] Process::ScriptChangeResult ProcessModel::setProgram(const JS::QmlSource& script)
50✔
582
{
583
  setExecutionScript(script.execution);
50✔
584
  setUiScript(script.ui);
50✔
585

586
  Process::ScriptChangeResult res;
50✔
587
  const auto trimmed = script.execution.trimmed();
50✔
588
  const QByteArray data = trimmed.toUtf8();
50✔
589

590
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
50✔
591

592
  // setQmlData builds the UI component from m_program.ui: it must be the new
593
  // one, otherwise every compile shows the UI of the previous compile.
594
  const auto previous = m_program;
50✔
595
  m_program = script;
50✔
596

597
  if(QFileInfo::exists(path))
50✔
598
    res = setQmlData(path.toUtf8(), true);
×
599
  else
600
    res = setQmlData(data, false);
50✔
601

602
  if(!res.valid)
50✔
603
    m_program = previous;
×
604
  return res;
50✔
605
}
50✔
606

607
Process::ScriptChangeResult ProcessModel::setQmlData(const QByteArray& data, bool isFile)
50✔
608
{
609
  Process::ScriptChangeResult res;
50✔
610
  if(!isFile && !data.contains("import "))
50✔
611
    return res;
×
612

613
  auto script = m_cache.getExecution(*this, data, isFile);
50✔
614
  if(!script)
50✔
UNCOV
615
    return res;
×
616

617
  m_isFile = isFile;
50✔
618
  m_qmlData = data;
50✔
619

620
  res.inlets = score::clearAndDeleteLater(m_inlets);
50✔
621
  res.outlets = score::clearAndDeleteLater(m_outlets);
50✔
622
  const bool had_ui = m_ui_component;
50✔
623
  m_ui_component = nullptr;
50✔
624
  delete m_ui_object;
50✔
625
  m_ui_object = nullptr;
50✔
626

627
  SCORE_ASSERT(m_inlets.size() == 0);
50✔
628
  SCORE_ASSERT(m_outlets.size() == 0);
50✔
629

630
  // Check inlets / outlets
631
  {
632
    auto cld_inlet = script->findChildren<Inlet*>();
50✔
633
    int i = 0;
50✔
634
    for(auto n : cld_inlet)
143✔
635
    {
636
      auto port = n->make(Id<Process::Port>(i++), this);
93✔
637
      if(const auto& name = n->objectName(); !name.isEmpty())
179✔
638
        port->setName(name);
86✔
639
      if(auto addr = State::parseAddressAccessor(n->address()))
93✔
UNCOV
640
        port->setAddress(std::move(*addr));
×
641
      m_inlets.push_back(port);
93✔
642
    }
643
  }
50✔
644

645
  {
646
    auto cld_outlet = script->findChildren<Outlet*>();
50✔
647
    int i = 0;
50✔
648
    for(auto n : cld_outlet)
93✔
649
    {
650
      auto port = n->make(Id<Process::Port>(i++), this);
43✔
651
      if(const auto& name = n->objectName(); !name.isEmpty())
86✔
652
        port->setName(name);
43✔
653
      if(auto addr = State::parseAddressAccessor(n->address()))
43✔
UNCOV
654
        port->setAddress(std::move(*addr));
×
655
      m_outlets.push_back(port);
43✔
656
    }
657
  }
50✔
658

659
  // Create ui if any
660
  if(!this->m_program.ui.isEmpty()) {
50✔
661
    m_ui_component = m_cache.getUi(*this, this->m_program.ui.toUtf8(), isFile);
13✔
662
  }
13✔
663

664
  if(m_isFile)
50✔
665
  {
UNCOV
666
    const auto name = QFileInfo{data}.baseName();
×
UNCOV
667
    metadata().setName(name);
×
UNCOV
668
    metadata().setLabel(name);
×
UNCOV
669
  }
×
670
  else if(metadata().getName().isEmpty())
50✔
671
  {
672
    metadata().setName(QStringLiteral("Script"));
32✔
673
  }
32✔
674

675
  executionScriptOk();
50✔
676
  res.valid = true;
50✔
677

678
  if(bool(m_ui_component) != had_ui)
50✔
679
    flagsChanged();
4✔
680

681
  if(m_ui_component)
50✔
682
  {
683
    uiScriptOk();
7✔
684
  }
7✔
685
  else if(externalUI)
43✔
686
  {
UNCOV
687
    externalUI->close();
×
UNCOV
688
    externalUI->deleteLater();
×
UNCOV
689
    externalUI = nullptr;
×
UNCOV
690
    externalUIVisible(false);
×
UNCOV
691
  }
×
692

693
  // inlets / outletsChanged : in ScriptEditCommand
694
  return res;
50✔
695
}
50✔
696

697
Script* ProcessModel::currentExecutionObject() const noexcept
×
698
{
UNCOV
699
  if(auto cache = m_cache.tryGet(m_qmlData, m_isFile))
×
UNCOV
700
    return cache->object.get();
×
UNCOV
701
  return nullptr;
×
UNCOV
702
}
×
703

704
bool ProcessModel::isGpu() const noexcept
×
705
{
706
#if defined(SCORE_HAS_GPU_JS)
707
  if(auto script = currentExecutionObject())
708
  {
709
    return
710
        script->findChild<JS::TextureInlet*>() != nullptr
711
           || script->findChild<JS::TextureOutlet*>() != nullptr
712
           // || script->findChild<JS::BufferInlet*>() != nullptr
713
           // || script->findChild<JS::BufferOutlet*>() != nullptr
714
        ;
715
  }
716
#endif
UNCOV
717
  return false;
×
718
}
719

720
ComponentCache::ComponentCache() { }
37✔
721
ComponentCache::~ComponentCache() { }
37✔
722

723
const ComponentCache::Cache* ComponentCache::tryGet(const QByteArray& str, bool isFile) const noexcept
69✔
724
{
725
  QByteArray content;
69✔
726
  QFile f;
69✔
727
  if(isFile)
69✔
728
  {
UNCOV
729
    f.setFileName(str);
×
UNCOV
730
    if(f.open(QIODevice::ReadOnly))
×
UNCOV
731
      content = score::mapAsByteArray(f);
×
732
    else
UNCOV
733
      return nullptr;
×
UNCOV
734
  }
×
735
  else
736
  {
737
    content = str;
69✔
738
  }
739

740
  if(auto it = ossia::find_if(m_map, [&](const auto& k) { return k.key == content; });
116✔
741
     it != m_map.end())
69✔
742
  {
743
    return &*it;
17✔
744
  }
745
  return nullptr;
52✔
746
}
69✔
747

748
Script* ComponentCache::getExecution(
56✔
749
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
750
{
751
  if(auto cache = tryGet(str, isFile))
56✔
752
    return cache->object.get();
15✔
753

754
  auto& dummyEngine = score::GUIAppContext()
82✔
755
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
41✔
756
                          .m_scriptProcessUIEngine;
41✔
757
  std::unique_ptr<QQmlComponent> comp;
41✔
758
  if(!isFile)
41✔
759
  {
760
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
41✔
761
    loadJSObjectFromString(process.rootPath(), str, *comp, false);
41✔
762
  }
41✔
763
  else
764
  {
UNCOV
765
    comp = std::make_unique<QQmlComponent>(&dummyEngine, QUrl::fromLocalFile(str));
×
766
  }
767

768
  const auto& errs = comp->errors();
41✔
769
  if(!errs.empty())
41✔
770
  {
UNCOV
771
    const auto& err = errs.first();
×
772
    qDebug() << err.line() << err.toString();
×
UNCOV
773
    auto str = err.toString();
×
UNCOV
774
    str.remove("<Unknown File>:");
×
UNCOV
775
    process.errorMessage(/* err.line(), */str);
×
UNCOV
776
    return nullptr;
×
UNCOV
777
  }
×
778

779
  auto obj = comp->create();
41✔
780
  auto script = qobject_cast<JS::Script*>(obj);
41✔
781
  if(script)
41✔
782
  {
783
    if(m_map.size() > 5)
41✔
784
      m_map.erase(m_map.begin());
×
785

786
    m_map.emplace_back(
82✔
787
        Cache{str, std::move(comp), std::unique_ptr<JS::Script>(script)});
41✔
788
    return script;
41✔
789
  }
790
  else
791
  {
UNCOV
792
    process.errorMessage(/* 0, */"The component must be of type Script");
×
UNCOV
793
    if(obj)
×
794
    {
UNCOV
795
      delete obj;
×
UNCOV
796
    }
×
UNCOV
797
    return nullptr;
×
798
  }
799
}
56✔
800

801
QQmlComponent* ComponentCache::getUi(
13✔
802
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
803
{
804
  if(auto cache = tryGet(str, isFile))
13✔
805
    return cache->component.get();
2✔
806

807
  auto& dummyEngine = score::GUIAppContext()
22✔
808
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
11✔
809
                          .m_scriptProcessUIEngine;
11✔
810

811
  std::unique_ptr<QQmlComponent> comp;
11✔
812
  if(!isFile)
11✔
813
  {
814
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
11✔
815
    loadJSObjectFromString(process.rootPath(), str, *comp, true);
11✔
816
  }
11✔
817
  else
818
  {
UNCOV
819
    comp = std::make_unique<QQmlComponent>(&dummyEngine, QUrl::fromLocalFile(str));
×
820
  }
821

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

833
  auto obj = comp->beginCreate(dummyEngine.rootContext());
11✔
834
  if(!obj) {
11✔
835
    process.errorMessage(/* 0, */"Cannot create UI object");
×
836
    return nullptr;
×
837
  }
838
  auto script = qobject_cast<ScriptUI*>(obj);
11✔
839
  if(script) {
11✔
840
    script->setProcess((Process::ProcessModel*)&process);
5✔
841
  }
5✔
842
  comp->completeCreate();
11✔
843
  if(script)
11✔
844
  {
845
    if(m_map.size() > 5)
5✔
UNCOV
846
      m_map.erase(m_map.begin());
×
847

848
    m_map.emplace_back(
10✔
849
        Cache{str, std::move(comp), {}});
5✔
850
    delete script;
5✔
851
    return m_map.back().component.get();
5✔
852
  }
853
  else
854
  {
855
    process.errorMessage(/* 0, */"The component must be of type Script");
6✔
856
    if(obj)
6✔
857
      delete obj;
6✔
858
    return nullptr;
6✔
859
  }
860
}
13✔
861

UNCOV
862
void ProcessModel::loadPreset(const Process::Preset& preset)
×
863
{
UNCOV
864
  Process::loadScriptProcessPreset<ProcessModel::p_program>(*this, preset);
×
UNCOV
865
}
×
866

UNCOV
867
Process::Preset ProcessModel::savePreset() const noexcept
×
868
{
869
  // FIXME this should save p_program
UNCOV
870
  return Process::saveScriptProcessPreset(*this, this->m_qmlData);
×
871
}
872

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